├── .travis.yml ├── .versions ├── package.js ├── LICENSE ├── client.coffee ├── README.md ├── server.coffee └── tests.coffee /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | sudo: required 5 | before_install: 6 | - "curl -L http://git.io/ejPSng | /bin/sh" 7 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | allow-deny@1.0.5 2 | babel-compiler@6.18.2 3 | babel-runtime@1.0.1 4 | base64@1.0.10 5 | binary-heap@1.0.10 6 | blaze@2.1.8 7 | blaze-tools@1.0.9 8 | boilerplate-generator@1.0.11 9 | caching-compiler@1.1.9 10 | callback-hook@1.0.10 11 | check@1.2.5 12 | coffeescript@1.11.1_3 13 | ddp@1.2.5 14 | ddp-client@1.3.4 15 | ddp-common@1.2.8 16 | ddp-server@1.3.14 17 | deps@1.0.12 18 | diff-sequence@1.0.7 19 | ecmascript@0.7.2 20 | ecmascript-runtime@0.3.15 21 | ejson@1.0.13 22 | geojson-utils@1.0.10 23 | html-tools@1.0.10 24 | htmljs@1.0.10 25 | id-map@1.0.9 26 | jquery@1.11.10 27 | local-test:peerlibrary:control-mergebox@0.3.0 28 | logging@1.1.17 29 | meteor@1.6.1 30 | minimongo@1.0.21 31 | modules@0.8.2 32 | modules-runtime@0.7.10 33 | mongo@1.1.16 34 | mongo-id@1.0.6 35 | npm-mongo@2.2.24 36 | observe-sequence@1.0.16 37 | ordered-dict@1.0.9 38 | peerlibrary:assert@0.2.5 39 | peerlibrary:classy-test@0.2.26 40 | peerlibrary:control-mergebox@0.3.0 41 | peerlibrary:extend-publish@0.4.0 42 | peerlibrary:fiber-utils@0.6.0 43 | promise@0.8.8 44 | random@1.0.10 45 | reactive-var@1.0.11 46 | retry@1.0.9 47 | routepolicy@1.0.12 48 | spacebars@1.0.12 49 | spacebars-compiler@1.0.12 50 | tinytest@1.0.12 51 | tracker@1.1.2 52 | ui@1.0.11 53 | underscore@1.0.10 54 | webapp@1.3.15 55 | webapp-hashing@1.0.9 56 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "Control mergebox of publish endpoints", 3 | version: '0.4.0', 4 | name: 'peerlibrary:control-mergebox', 5 | git: 'https://github.com/peerlibrary/meteor-control-mergebox.git' 6 | }); 7 | 8 | Package.onUse(function (api) { 9 | api.versionsFrom('METEOR@1.8.1'); 10 | 11 | // Core dependencies. 12 | api.use([ 13 | 'coffeescript@2.4.1', 14 | 'underscore', 15 | 'mongo', 16 | 'ddp', 17 | 'ejson', 18 | 'mongo-id' 19 | ]); 20 | 21 | // 3rd party dependencies. 22 | api.use([ 23 | 'peerlibrary:fiber-utils@0.10.0', 24 | 'peerlibrary:extend-publish@0.6.0' 25 | ], 'server'); 26 | 27 | api.addFiles([ 28 | 'server.coffee' 29 | ], 'server'); 30 | 31 | api.addFiles([ 32 | 'client.coffee' 33 | ], 'client'); 34 | }); 35 | 36 | Package.onTest(function (api) { 37 | api.versionsFrom('METEOR@1.8.1'); 38 | 39 | // Core dependencies. 40 | api.use([ 41 | 'coffeescript@2.4.1', 42 | 'underscore', 43 | 'mongo' 44 | ]); 45 | 46 | // Internal dependencies. 47 | api.use([ 48 | 'peerlibrary:control-mergebox' 49 | ]); 50 | 51 | // 3rd party dependencies. 52 | api.use([ 53 | 'peerlibrary:classy-test@0.4.0' 54 | ]); 55 | 56 | api.add_files([ 57 | 'tests.coffee' 58 | ]); 59 | }); 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, The PeerLibrary Project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the PeerLibrary Project nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /client.coffee: -------------------------------------------------------------------------------- 1 | Connection = Meteor.connection.constructor 2 | 3 | # We patch registerStore to intercept messages and modify them to not throw errors. 4 | originalRegisterStore = Connection::registerStore 5 | Connection::registerStore = (name, wrappedStore) -> 6 | originalUpdate = wrappedStore.update 7 | wrappedStore.update = (msg) -> 8 | collection = wrappedStore._getCollection?() 9 | 10 | # We might still not have a collection for packages defining collections before 11 | # this package is loaded. But this is OK because those are packages which do not 12 | # use this package on their collections. If you want to use this package on your 13 | # collections you have to anyway define a dependency on it. 14 | return originalUpdate.call @, msg unless collection 15 | 16 | mongoId = MongoID.idParse msg.id 17 | doc = collection._collection._docs.get mongoId 18 | 19 | # If a document is being added, but already exists, just change it. 20 | if msg.msg is 'added' and doc 21 | msg.msg = 'changed' 22 | # If a document is being removed, but it is already removed, do not do anything. 23 | else if msg.msg is 'removed' and not doc 24 | return 25 | # If a document is being changed, but it does not yet exist, just add it. 26 | else if msg.msg is 'changed' and not doc 27 | msg.msg = 'added' 28 | 29 | # We do not want to pass on fields marked for clearing. 30 | for field, value of msg.fields when value is undefined 31 | delete msg.fields[field] 32 | 33 | originalUpdate.call @, msg 34 | 35 | originalRegisterStore.call @, name, wrappedStore 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | control-mergebox 2 | ================ 3 | 4 | UPDATE: This is now available in [Meteor core](https://docs.meteor.com/api/pubsub.html#Publication-strategies). 5 | 6 | This Meteor smart package extends [publish endpoints](http://docs.meteor.com/#/full/meteor_publish) 7 | with control of the [mergebox](https://meteorhacks.com/understanding-mergebox) for a given 8 | publish endpoint function. 9 | 10 | Publish function's `this` is extended with `this.disableMergebox()` which when called will 11 | [disable mergebox](https://github.com/meteor/meteor/issues/5645) for current publish endpoint. 12 | 13 | By disabling mergebox one chooses to send possibly unnecessary data to clients (because 14 | they already have it) and not maintain on the server side images of clients' data, thus 15 | reducing CPU and memory load on servers. 16 | 17 | Server side only (with compatibility changes on the client side). 18 | 19 | Installation 20 | ------------ 21 | 22 | ``` 23 | meteor add peerlibrary:control-mergebox 24 | ``` 25 | 26 | Discussion 27 | ---------- 28 | 29 | This package disables storing the image of client's data on the server side for a given 30 | publish endpoint. This is useful to reduce CPU and memory load on servers, but it makes 31 | server unable to do some things it could do before: 32 | 33 | Server does not know which fields client already has and what are their values, 34 | so if your publish function, for example, calls `this.changed('collectionName', id, {foo: 'bar'})` 35 | twice in a row, server will not suppress sending the change twice. This is not so problematic 36 | because it keeps the semantics unchanged, just makes more data go over the wire. 37 | 38 | Server also does not know which documents were published from which subscription. This 39 | is more problematic. With mergebox, Meteor tracks which subscription published which documents 40 | (with which fields) and if a document with same ID (and with possibly overlapping fields) 41 | is published from multiple subscriptions, Meteor knows what to do when one subscription removes 42 | a document (or a field) which still exists in other subscriptions. It removes just the fields 43 | previously published by this subscription only, while keeping all other fields for the document 44 | published. With disabled mergebox, when one subscription removes a document or a field that 45 | change is propagated immediately to the client as it is. This is different semantics to 46 | the one with enabled mergebox. So it is important to remember, **with disabled mergebox, 47 | the last document or field change across all subscriptions is always the one propagated to 48 | the client side**. There is simply no state in subscriptions anymore. 49 | 50 | The default way to publish collections is to use 51 | [`observeChanges`](http://docs.meteor.com/#/full/observe_changes), explicitly or by returning 52 | a cursor from the publish function. This works well when server uses mergebox because only 53 | changes are really needed. But if you use multiple subscriptions over the same collection with 54 | disabled mergebox you might get strange results. For example, one subscription could remove 55 | a document (which would remove whole document on the client side) and then another subscription 56 | could change one field, which would result in document being re-added on the client side, but 57 | just with that one field. To improve this, one could use 58 | [document-level `observe`](http://docs.meteor.com/#/full/observe): 59 | 60 | ```javascript 61 | Meteor.publish('myPublish', function () { 62 | var self = this; 63 | 64 | var handle = collection.find({}, {transform: null}).observe({ 65 | added: function (newDocument) { 66 | self.added('testCollection', newDocument._id, _.omit(newDocument, '_id')); 67 | }, 68 | 69 | changed: function (newDocument, oldDocument) { 70 | _.each(oldDocument, function (value, field) { 71 | if (!_.has(newDocument, field)) { 72 | newDocument[field] = undefined; 73 | } 74 | }); 75 | 76 | self.changed('testCollection', newDocument._id, _.omit(newDocument, '_id')); 77 | }, 78 | 79 | removed: function (oldDocument) { 80 | self.removed('testCollection', oldDocument._id); 81 | } 82 | }); 83 | 84 | self.onStop(function () { 85 | handle.stop() 86 | }); 87 | 88 | self.ready(); 89 | }); 90 | ``` 91 | 92 | But the question is then how much overhead do you gain on the server by using `observe` instead of 93 | `observeChanges`, because `observe` has to cache documents. 94 | 95 | In general it is advisable to not use multiple overlapping (in documents) subscriptions when 96 | disabling mergebox. Behavior is then predictable and matches the normal Meteor behavior. 97 | 98 | Related projects 99 | ---------------- 100 | 101 | * [meteor-streams](https://arunoda.github.io/meteor-streams/) – allows sending of messages 102 | from server to client without a mergebox, but then requires manual handling of those 103 | messages on the client, not using features already provided by Meteor 104 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | guardObject = {} 2 | 3 | extendPublish (name, publishFunction, options) -> 4 | newPublishFunction = (args...) -> 5 | publish = @ 6 | 7 | disabled = false 8 | 9 | publish.disableMergebox = -> 10 | disabled = true 11 | 12 | originalAdded = publish.added 13 | publish.added = (collectionName, id, fields) -> 14 | return originalAdded.call @, collectionName, id, fields unless disabled 15 | 16 | stringId = @_idFilter.idStringify id 17 | 18 | FiberUtils.synchronize guardObject, "#{collectionName}$#{stringId}", => 19 | collectionView = @_session.getCollectionView collectionName 20 | 21 | if collectionView.documents instanceof Map 22 | originalSessionDocumentView = collectionView.documents.get(stringId) 23 | 24 | try 25 | # Make sure we start with a clean slate for this document ID. 26 | collectionView.documents.delete(stringId) 27 | 28 | originalAdded.call @, collectionName, id, fields 29 | finally 30 | if originalSessionDocumentView 31 | collectionView.documents.set(stringId, originalSessionDocumentView) 32 | else 33 | collectionView.documents.delete(stringId) 34 | 35 | else 36 | originalSessionDocumentView = collectionView.documents[stringId] 37 | 38 | try 39 | # Make sure we start with a clean slate for this document ID. 40 | delete collectionView.documents[stringId] 41 | 42 | originalAdded.call @, collectionName, id, fields 43 | finally 44 | if originalSessionDocumentView 45 | collectionView.documents[stringId] = originalSessionDocumentView 46 | else 47 | delete collectionView.documents[stringId] 48 | 49 | originalChanged = publish.changed 50 | publish.changed = (collectionName, id, fields) -> 51 | return originalChanged.call @, collectionName, id, fields unless disabled 52 | 53 | stringId = @_idFilter.idStringify id 54 | 55 | FiberUtils.synchronize guardObject, "#{collectionName}$#{stringId}", => 56 | collectionView = @_session.getCollectionView collectionName 57 | 58 | if collectionView.documents instanceof Map 59 | originalSessionDocumentView = collectionView.documents.get(stringId) 60 | 61 | try 62 | # Create an empty session document for this id. 63 | collectionView.documents.set(id, new DDPServer._SessionDocumentView()) 64 | 65 | # For fields which are being cleared we have to mock some existing 66 | # value otherwise change will not be send to the client. 67 | for field, value of fields when value is undefined 68 | collectionView.documents.get(id).dataByKey.set(field, [subscriptionHandle: @_subscriptionHandle, value: null]) 69 | 70 | originalChanged.call @, collectionName, id, fields 71 | finally 72 | if originalSessionDocumentView 73 | collectionView.documents.set(stringId, originalSessionDocumentView) 74 | else 75 | collectionView.documents.delete(stringId) 76 | 77 | else 78 | originalSessionDocumentView = collectionView.documents[stringId] 79 | 80 | try 81 | # Create an empty session document for this id. 82 | collectionView.documents[id] = new DDPServer._SessionDocumentView() 83 | 84 | # For fields which are being cleared we have to mock some existing 85 | # value otherwise change will not be send to the client. 86 | for field, value of fields when value is undefined 87 | collectionView.documents[id].dataByKey[field] = [subscriptionHandle: @_subscriptionHandle, value: null] 88 | 89 | originalChanged.call @, collectionName, id, fields 90 | finally 91 | if originalSessionDocumentView 92 | collectionView.documents[stringId] = originalSessionDocumentView 93 | else 94 | delete collectionView.documents[stringId] 95 | 96 | originalRemoved = publish.removed 97 | publish.removed = (collectionName, id) -> 98 | return originalRemoved.call @, collectionName, id unless disabled 99 | 100 | stringId = @_idFilter.idStringify id 101 | 102 | FiberUtils.synchronize guardObject, "#{collectionName}$#{stringId}", => 103 | collectionView = @_session.getCollectionView collectionName 104 | 105 | if collectionView.documents instanceof Map 106 | originalSessionDocumentView = collectionView.documents.get(stringId) 107 | 108 | try 109 | # Create an empty session document for this id. 110 | collectionView.documents.set(id, new DDPServer._SessionDocumentView()) 111 | 112 | originalRemoved.call @, collectionName, id 113 | finally 114 | if originalSessionDocumentView 115 | collectionView.documents.set(stringId, originalSessionDocumentView) 116 | else 117 | collectionView.documents.delete(stringId) 118 | 119 | else 120 | originalSessionDocumentView = collectionView.documents[stringId] 121 | 122 | try 123 | # Create an empty session document for this id. 124 | collectionView.documents[id] = new DDPServer._SessionDocumentView() 125 | 126 | originalRemoved.call @, collectionName, id 127 | finally 128 | if originalSessionDocumentView 129 | collectionView.documents[stringId] = originalSessionDocumentView 130 | else 131 | delete collectionView.documents[stringId] 132 | 133 | publishFunction.apply publish, args 134 | 135 | [name, newPublishFunction, options] 136 | -------------------------------------------------------------------------------- /tests.coffee: -------------------------------------------------------------------------------- 1 | if Meteor.isServer 2 | TestCollection = new Mongo.Collection null 3 | 4 | Meteor.methods 5 | insertTest: (obj) -> 6 | TestCollection.insert obj 7 | 8 | updateTest: (selector, query) -> 9 | TestCollection.update selector, query 10 | 11 | removeTest: (selector) -> 12 | TestCollection.remove selector 13 | 14 | # "argument" is used so that we can subscribe multiple times with different arguments. 15 | Meteor.publish 'testPublish', (disableMergebox, argument) -> 16 | @disableMergebox() if disableMergebox 17 | 18 | handle = TestCollection.find().observeChanges 19 | added: (id, fields) => 20 | @added 'testCollection', id, fields 21 | changed: (id, fields) => 22 | @changed 'testCollection', id, fields 23 | removed: (id) => 24 | @removed 'testCollection', id 25 | 26 | @onStop => 27 | handle.stop() 28 | 29 | @ready() 30 | 31 | # "argument" is used so that we can subscribe multiple times with different arguments. 32 | Meteor.publish 'testPublishFullObserve', (argument) -> 33 | @disableMergebox() 34 | 35 | handle = TestCollection.find({}, {transform: null}).observe 36 | added: (newDocument) => 37 | @added 'testCollection', newDocument._id, _.omit newDocument, '_id' 38 | changed: (newDocument, oldDocument) => 39 | for field, value of oldDocument when field not of newDocument 40 | newDocument[field] = undefined 41 | @changed 'testCollection', newDocument._id, _.omit newDocument, '_id' 42 | removed: (oldDocument) => 43 | @removed 'testCollection', oldDocument._id 44 | 45 | @onStop => 46 | handle.stop() 47 | 48 | @ready() 49 | 50 | else 51 | TestCollection = new Mongo.Collection 'testCollection' 52 | 53 | class BasicTestCase extends ClassyTestCase 54 | @testName: 'disable-mergebox - basic' 55 | 56 | setUpServer: -> 57 | TestCollection.remove {} 58 | 59 | @publishTest: (disableMergebox) -> 60 | [ 61 | -> 62 | @assertSubscribeSuccessful 'testPublish', disableMergebox, @expect() 63 | , 64 | -> 65 | @assertEqual TestCollection.find().fetch(), [] 66 | 67 | Meteor.call 'insertTest', {foo: 'test', bar: 123}, @expect (error, documentId) => 68 | @assertFalse error, error 69 | @assertTrue documentId 70 | 71 | @document1Id = documentId 72 | , 73 | @runOnServer -> 74 | for sessionId, session of Meteor.server.sessions 75 | if disableMergebox 76 | @assertEqual session.getCollectionView('testCollection').documents, {} 77 | else 78 | @assertNotEqual session.getCollectionView('testCollection').documents, {} 79 | , 80 | -> 81 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test', bar: 123}] 82 | 83 | Meteor.call 'updateTest', {_id: @document1Id}, {$set: {foo: 'test2'}}, @expect (error, count) => 84 | @assertFalse error, error 85 | @assertEqual count, 1 86 | , 87 | @runOnServer -> 88 | for sessionId, session of Meteor.server.sessions 89 | if disableMergebox 90 | @assertEqual session.getCollectionView('testCollection').documents, {} 91 | else 92 | @assertNotEqual session.getCollectionView('testCollection').documents, {} 93 | , 94 | -> 95 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test2', bar: 123}] 96 | 97 | Meteor.call 'updateTest', {_id: @document1Id}, {$unset: {foo: ''}}, @expect (error, count) => 98 | @assertFalse error, error 99 | @assertEqual count, 1 100 | , 101 | @runOnServer -> 102 | for sessionId, session of Meteor.server.sessions 103 | if disableMergebox 104 | @assertEqual session.getCollectionView('testCollection').documents, {} 105 | else 106 | @assertNotEqual session.getCollectionView('testCollection').documents, {} 107 | , 108 | -> 109 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, bar: 123}] 110 | 111 | Meteor.call 'updateTest', {_id: @document1Id}, {$set: {foo: 'test3'}}, @expect (error, count) => 112 | @assertFalse error, error 113 | @assertEqual count, 1 114 | , 115 | @runOnServer -> 116 | for sessionId, session of Meteor.server.sessions 117 | if disableMergebox 118 | @assertEqual session.getCollectionView('testCollection').documents, {} 119 | else 120 | @assertNotEqual session.getCollectionView('testCollection').documents, {} 121 | , 122 | -> 123 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test3', bar: 123}] 124 | 125 | Meteor.call 'removeTest', {_id: @document1Id}, @expect (error, count) => 126 | @assertFalse error, error 127 | @assertEqual count, 1 128 | , 129 | @runOnServer -> 130 | for sessionId, session of Meteor.server.sessions 131 | @assertEqual session.getCollectionView('testCollection').documents, {} 132 | , 133 | -> 134 | @assertEqual TestCollection.find().fetch(), [] 135 | ] 136 | 137 | testClientMergeboxDisabled: @publishTest true 138 | 139 | testClientMergeboxNotDisabled: @publishTest false 140 | 141 | testClientMultipleSubscriptions: [ 142 | -> 143 | @subscription = @assertSubscribeSuccessful 'testPublish', true, 1, @expect() 144 | @assertSubscribeSuccessful 'testPublish', true, 2, @expect() 145 | , 146 | -> 147 | @assertEqual TestCollection.find().fetch(), [] 148 | 149 | Meteor.call 'insertTest', {foo: 'test', bar: 123}, @expect (error, documentId) => 150 | @assertFalse error, error 151 | @assertTrue documentId 152 | 153 | @document1Id = documentId 154 | , 155 | -> 156 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test', bar: 123}] 157 | 158 | @subscription.stop() 159 | 160 | # To wait a bit for unsubscribe to happen. 161 | Meteor.setTimeout @expect(), 100 # ms 162 | , 163 | -> 164 | # Last change wins, document has been removed. 165 | @assertEqual TestCollection.find().fetch(), [] 166 | 167 | Meteor.call 'updateTest', {_id: @document1Id}, {$set: {foo: 'test2'}}, @expect (error, count) => 168 | @assertFalse error, error 169 | @assertEqual count, 1 170 | , 171 | -> 172 | # Only the changed field is published. 173 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test2'}] 174 | ] 175 | 176 | testClientMultipleSubscriptionsFullObserve: [ 177 | -> 178 | @subscription = @assertSubscribeSuccessful 'testPublishFullObserve', 1, @expect() 179 | @assertSubscribeSuccessful 'testPublishFullObserve', 2, @expect() 180 | , 181 | -> 182 | @assertEqual TestCollection.find().fetch(), [] 183 | 184 | Meteor.call 'insertTest', {foo: 'test', bar: 123}, @expect (error, documentId) => 185 | @assertFalse error, error 186 | @assertTrue documentId 187 | 188 | @document1Id = documentId 189 | , 190 | -> 191 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test', bar: 123}] 192 | 193 | @subscription.stop() 194 | 195 | # To wait a bit for unsubscribe to happen. 196 | Meteor.setTimeout @expect(), 100 # ms 197 | , 198 | -> 199 | # Last change wins, document has been removed. 200 | @assertEqual TestCollection.find().fetch(), [] 201 | 202 | Meteor.call 'updateTest', {_id: @document1Id}, {$set: {foo: 'test2'}}, @expect (error, count) => 203 | @assertFalse error, error 204 | @assertEqual count, 1 205 | , 206 | -> 207 | # With full observe all fields are published. 208 | @assertEqual TestCollection.find().fetch(), [{_id: @document1Id, foo: 'test2', bar: 123}] 209 | ] 210 | 211 | ClassyTestCase.addTest new BasicTestCase() 212 | --------------------------------------------------------------------------------