├── .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 |
79 |
80 |
81 | Item |
82 | Description |
83 | Quantity |
84 |
85 | {{#each shoppingCartItems}}
86 |
87 | {{item}} |
88 | {{desc}} |
89 | {{quantity}} |
90 |
91 | {{/each}}
92 |
93 |
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 |
--------------------------------------------------------------------------------