├── .gitignore ├── .versions ├── README.md ├── license.txt ├── package.js ├── persistent-minimongo-tests.js └── persistent-minimongo.js /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | amplify@1.0.0 2 | base64@1.0.3 3 | binary-heap@1.0.3 4 | callback-hook@1.0.3 5 | check@1.0.5 6 | ddp@1.1.0 7 | ejson@1.0.6 8 | frozeman:persistent-minimongo@0.1.5 9 | geojson-utils@1.0.3 10 | id-map@1.0.3 11 | jquery@1.11.3_2 12 | json@1.0.3 13 | local-test:frozeman:persistent-minimongo@0.1.5 14 | logging@1.0.7 15 | meteor@1.1.6 16 | minimongo@1.0.8 17 | mongo@1.1.0 18 | ordered-dict@1.0.3 19 | random@1.0.3 20 | retry@1.0.3 21 | tinytest@1.0.5 22 | tracker@1.0.7 23 | underscore@1.0.3 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meteor persistent minimongo 2 | 3 | Simple client-side observer class that provides persistence for Meteor Collections using browser storage via Amplify.js. Collections are reactive across browser tabs. 4 | 5 | *based on a package by Jeff Mitchel https://github.com/jeffmitchel/meteor-local-persist* 6 | 7 | ## Note 8 | 9 | This package is using `localstorage` to persist your collections, 10 | if you want to use the browsers indexedDB or webSQL use the new [frozeman:persistent-minimongo2](https://atmospherejs.com/frozeman/persistent-minimongo2) package! 11 | 12 | ## Installation: 13 | `$ meteor add frozeman:persistent-minimongo` 14 | 15 | 16 | ## Documentation: 17 | 18 | ### Constructor: 19 | 20 | ``` 21 | new PersistentMinimongo(collection); 22 | ``` 23 | 24 | Collection is the Meteor Collection to be persisted. 25 | 26 | ### Methods: 27 | 28 | ```js 29 | 30 | var myPersistentColleciton = new PersistentMinimongo(collection); 31 | 32 | // Refreshes the collections from localstorage 33 | myPersistentColleciton.refresh() 34 | 35 | // Gets the current size of the localstorage in MB 36 | myPersistentColleciton.localStorageSize() 37 | 38 | // Will check if the current size of the localstorage is larger then 4.8 MB, if so it will remove the 50 latest entries of the collection. 39 | myPersistentColleciton.capCollection() 40 | ``` 41 | 42 | ## Example: 43 | 44 | Implement a simple shopping cart as a local collection. 45 | 46 | ```js 47 | if (Meteor.isClient) { 48 | // create a local collection, 49 | var shoppingCart = new Meteor.Collection('shopping-cart', {connection: null}); 50 | 51 | // create a local persistence observer 52 | var shoppingCartObserver = new PersistentMinimongo(shoppingCart); 53 | 54 | // create a handlebars helper to fetch the data 55 | Handlebars.registerHelper("shoppingCartItems", function () { 56 | return shoppingCart.find(); 57 | }); 58 | 59 | // that's it. just use the collection normally and the observer 60 | // will keep it sync'd to browser storage. the data will be stored 61 | // back into the collection when returning to the app (depending, 62 | // of course, on availability of localStorage in the browser). 63 | 64 | shoppingCart.insert({ item: 'DMB-01', desc: 'Discover Meteor Book', quantity: 1 }); 65 | }); 66 | } 67 | ``` 68 | 69 | ```html 70 | 71 | Shopping Cart 72 | 73 | 74 | 75 | {{> shoppingCart}} 76 | 77 | 78 | 94 | ``` 95 | 96 | ## Notes: 97 | 98 | - This is a simple implementation that keeps an identical copy of the collection's data in browser storage. While not especially space efficient, it does preserve all of the Meteor.Collection reactive goodness. 99 | 100 | - The cross-tab reactvity implementation is naive and will resync all PersistentMinimongo instances when a browser storage event is fired. 101 | 102 | - See http://amplifyjs.com/api/store/#storagetypes for information about how data is stored in the browser. 103 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | =================================================== 2 | Licensed under the MIT License 3 | =================================================== 4 | 5 | Copyright (C) 2013 Jeff Mitchell / 3dotO, Inc. 6 | 7 | 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: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | 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. 12 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'frozeman:persistent-minimongo', 3 | summary: 'Persistent Client-side Collections for Meteor using localstorage', 4 | version: '0.1.8', 5 | git: 'http://github.com/frozeman/meteor-persistent-minimongo' 6 | }); 7 | 8 | Package.on_use(function (api) { 9 | api.versionsFrom('METEOR@1.0'); 10 | 11 | api.use('underscore', 'client'); 12 | api.use('amplify@1.0.0', 'client'); 13 | 14 | api.add_files('persistent-minimongo.js', 'client'); 15 | 16 | api.export('PersistentMinimongo', 'client'); 17 | }); 18 | 19 | Package.on_test(function (api) { 20 | // api.use('underscore', 'client'); 21 | // api.use('amplify', 'client'); 22 | api.use('tinytest', 'client'); 23 | api.use('frozeman:persistent-minimongo', 'client'); 24 | api.add_files('persistent-minimongo-tests.js', 'client'); 25 | }); -------------------------------------------------------------------------------- /persistent-minimongo-tests.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | { firstName: 'Albert', lastName: 'Einstein', email: 'emc2@princeton.edu' }, 3 | { firstName: 'Marie', lastName: 'Curie', email: 'marie.curie@sorbonne.fr' }, 4 | { firstName: 'Max', lastName: 'Planck', email: 'max@mpg.de' } 5 | ]; 6 | 7 | // test adding, retrieving and deleting data. the tests are a bit bogus since we can't 8 | // reload the browser to exercise the persistence. the best we can do is to verify that 9 | // amplify has stored the correct data. 10 | 11 | Tinytest.add('Local Persist - Insert Data', function(test) { 12 | var testCollection = new Mongo.Collection(null); 13 | var testObserver = new PersistentMinimongo(testCollection); 14 | 15 | data.forEach(function (doc) { 16 | testCollection.insert(doc); 17 | }); 18 | 19 | // right number of adds? 20 | test.equal(testObserver._getStats().added, data.length); 21 | 22 | // get the tracking list and verify it has the correct number of keys 23 | var list = amplify.store(testObserver._getKey()); 24 | test.equal(list.length, data.length); 25 | }); 26 | 27 | Tinytest.add('Local Persist - Retrieve Data', function(test) { 28 | var testCollection = new Mongo.Collection(null); 29 | var testObserver = new PersistentMinimongo(testCollection); 30 | 31 | // right number of adds? 32 | test.equal(testObserver._getStats().added, data.length); 33 | 34 | data.forEach(function (doc) { 35 | m = testCollection.findOne({ lastName: doc.lastName }); 36 | a = amplify.store(testObserver._makeDataKey(m._id)); 37 | test.equal(a, m); 38 | }); 39 | }); 40 | 41 | Tinytest.add('Local Persist - Remove Data', function(test) { 42 | var testCollection = new Mongo.Collection(null); 43 | var testObserver = new PersistentMinimongo(testCollection); 44 | 45 | // right number of adds? 46 | test.equal(testObserver._getStats().added, data.length); 47 | 48 | testCollection.remove({}); 49 | 50 | // right number of removes? 51 | test.equal(testObserver._getStats().removed, data.length); 52 | 53 | // the tracking list should be gone 54 | var list = amplify.store(testObserver._getKey()); 55 | test.equal(!! list, false); 56 | }); 57 | -------------------------------------------------------------------------------- /persistent-minimongo.js: -------------------------------------------------------------------------------- 1 | /** 2 | Packages 3 | 4 | @module Packages 5 | */ 6 | 7 | /** 8 | The PersistentMinimongo package 9 | 10 | @class PersistentMinimongo 11 | @constructor 12 | */ 13 | 14 | 15 | 16 | /** 17 | If the localstorage goes over 4.8 MB, trim the collections. 18 | 19 | @property capLocalStorageSize 20 | */ 21 | var capLocalStorageSize = 4.8; 22 | 23 | /** 24 | If the localstorage goes over `capLocalStorageSize`, trim the current collection, 25 | which wanted to add a new entry, by 50 entries. 26 | 27 | @property trimCollectionBy 28 | */ 29 | var trimCollectionBy = 50; 30 | 31 | 32 | PersistentMinimongo = function (collection) { 33 | var self = this; 34 | if (! (self instanceof PersistentMinimongo)) 35 | throw new Error('use "new" to construct a PersistentMinimongo'); 36 | 37 | self.key = 'minimongo__' + collection._name; 38 | self.col = collection; 39 | self.stats = { added: 0, removed: 0, changed: 0 }; 40 | 41 | persisters.push(self); 42 | 43 | // Check if the localstorage is to big and reduce the current collection by 50 items, every 30s 44 | Meteor.setInterval(function() { 45 | self.capCollection(); 46 | }, 1000 * 30); 47 | 48 | // load from storage 49 | self.refresh(true); 50 | 51 | // Meteor.startup(function () { 52 | self.col.find({}).observe({ 53 | added: function (doc) { 54 | 55 | // get or initialize tracking list 56 | var list = amplify.store(self.key); 57 | if (! list) 58 | list = []; 59 | 60 | // add document id to tracking list and store 61 | if (! _.contains(list, doc._id)) { 62 | list.push(doc._id); 63 | amplify.store(self.key, list); 64 | } 65 | 66 | // store copy of document into local storage, if not already there 67 | var key = self._makeDataKey(doc._id); 68 | if(! amplify.store(key)) { 69 | amplify.store(key, doc); 70 | } 71 | 72 | ++self.stats.added; 73 | }, 74 | 75 | removed: function (doc) { 76 | var list = amplify.store(self.key); 77 | 78 | // if not in list, nothing to do 79 | if(! _.contains(list, doc._id)) 80 | return; 81 | 82 | // remove from list 83 | list = _.without(list, doc._id); 84 | 85 | // remove document copy from local storage 86 | amplify.store(self._makeDataKey(doc._id), null); 87 | 88 | // if tracking list is empty, delete; else store updated copy 89 | amplify.store(self.key, list.length === 0 ? null : list); 90 | 91 | ++self.stats.removed; 92 | }, 93 | 94 | changed: function (newDoc, oldDoc) { 95 | // update document in local storage 96 | amplify.store(self._makeDataKey(newDoc._id), newDoc); 97 | ++self.stats.changed; 98 | } 99 | }); 100 | // }); 101 | }; 102 | 103 | PersistentMinimongo.prototype = { 104 | constructor: PersistentMinimongo, 105 | _getStats: function () { 106 | return this.stats; 107 | }, 108 | _getKey: function () { 109 | return this.key; 110 | }, 111 | _makeDataKey: function (id) { 112 | return this.key + '__' + id; 113 | }, 114 | /** 115 | Refresh the local storage 116 | 117 | @method refresh 118 | @return {String} 119 | */ 120 | refresh: function (init) { 121 | var self = this; 122 | var list = amplify.store(self.key); 123 | 124 | self.stats.added = 0; 125 | 126 | 127 | if (!! list) { 128 | var length = list.length; 129 | list = _.filter(list, function (id) { 130 | var doc = amplify.store(self._makeDataKey(id)); 131 | if(!! doc) { 132 | var id = doc._id; 133 | delete doc._id; 134 | self.col.upsert({_id: id}, {$set: doc}); 135 | } 136 | 137 | return !! doc; 138 | }); 139 | 140 | // if not initializing, check for deletes 141 | if(! init) { 142 | _.each(self.col.find({}).fetch(), function (doc) { 143 | if(! _.contains(list, doc._id)) 144 | self.col.remove({ _id: doc._id}); 145 | }); 146 | } 147 | 148 | // if initializing, save cleaned list (if changed) 149 | if(init && length != list.length) 150 | amplify.store(self.key, list.length === 0 ? null : list); 151 | } 152 | }, 153 | /** 154 | Gets the current localstorage size in MB 155 | 156 | @method localStorageSize 157 | @return {String} total localstorage size in MB 158 | */ 159 | localStorageSize: function() { 160 | 161 | // function toSizeMB(info) { 162 | // info.size = toMB(info.size).toFixed(2) + ' MB'; 163 | // return info; 164 | // } 165 | 166 | // var sizes = Object.keys(localStorage).map(toSize).map(toSizeMB); 167 | // console.table(sizes); 168 | 169 | var size = 0; 170 | if(localStorage) { 171 | _.each(Object.keys(localStorage), function(key){ 172 | size += localStorage[key].length * 2 / 1024 / 1024; 173 | }); 174 | } 175 | 176 | return size; 177 | }, 178 | /** 179 | Check if the localstorage is to big and reduce the current collection by 50 items 180 | 181 | @method localStorageSize 182 | @return {String} 183 | */ 184 | capCollection: function(){ 185 | var _this = this; 186 | 187 | if(_this.localStorageSize() > capLocalStorageSize) { 188 | console.log(_this.localStorageSize(), _this.col.find({}).count()); 189 | // find the first 50 entries and remove them 190 | _.each(_this.col.find({}, {limit: trimCollectionBy}).fetch(), function(item){ 191 | _this.col.remove(item._id); 192 | }); 193 | } 194 | } 195 | }; 196 | 197 | var persisters = []; 198 | var lpTimer = null; 199 | 200 | // React on manual local storage changes 201 | Meteor.startup(function () { 202 | $(window).bind('storage', function (e) { 203 | Meteor.clearTimeout(lpTimer); 204 | lpTimer = Meteor.setTimeout(function () { 205 | _.each(persisters, function (lp) { 206 | lp.refresh(false); 207 | }); 208 | }, 250); 209 | }); 210 | }); 211 | --------------------------------------------------------------------------------