├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .travis.yml ├── CHANGELOG.md ├── EVENTS.md ├── LICENSE.md ├── README.md ├── groundDB.client.tests.js ├── groundDB.server.tests.js ├── lib ├── client │ ├── ground.db.js │ ├── pending.jobs.js │ └── servertime.js └── server │ ├── ground.db.js │ └── servertime.js ├── package-lock.json ├── package.js └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # .editorconfig 2 | # Meteor adapted EditorConfig, http://EditorConfig.org 3 | # By RaiX 2013 4 | 5 | root = true 6 | 7 | [*.js] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | charset = utf-8 14 | max_line_length = 80 15 | indent_brace_style = 1TBS 16 | spaces_around_operators = true 17 | quote_type = auto 18 | # curly_bracket_next_line = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.versions 2 | /versions.json 3 | .build* 4 | .DS_Store 5 | .idea/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 6, 3 | "maxerr": 50, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "forin": true, 9 | "immed": false, 10 | "indent": 2, 11 | "latedef": false, 12 | "newcap": false, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonew": false, 16 | "plusplus": false, 17 | "quotmark": false, 18 | "undef": true, 19 | "unused": true, 20 | "strict": false, 21 | "trailing": true, 22 | "maxparams": false, 23 | "maxdepth": false, 24 | "maxstatements": false, 25 | "maxcomplexity": false, 26 | "maxlen": 140, 27 | "asi": false, 28 | "boss": false, 29 | "debug": false, 30 | "eqnull": false, 31 | "es5": false, 32 | "esnext": false, 33 | "moz": false, 34 | "evil": false, 35 | "expr": false, 36 | "funcscope": false, 37 | "globalstrict": true, 38 | "iterator": false, 39 | "lastsemic": false, 40 | "laxbreak": false, 41 | "laxcomma": false, 42 | "loopfunc": false, 43 | "multistr": false, 44 | "proto": false, 45 | "scripturl": false, 46 | "smarttabs": false, 47 | "shadow": false, 48 | "sub": false, 49 | "supernew": false, 50 | "validthis": false, 51 | "browser": true, 52 | "couch": false, 53 | "devel": true, 54 | "dojo": false, 55 | "jquery": false, 56 | "mootools": false, 57 | "node": false, 58 | "nonstandard": false, 59 | "prototypejs": false, 60 | "rhino": false, 61 | "worker": false, 62 | "wsh": false, 63 | "yui": false, 64 | "nomen": false, 65 | "onevar": false, 66 | "passfail": false, 67 | "white": false, 68 | "predef": [ 69 | "App", 70 | "Assets", 71 | "Cordova", 72 | "Deps", 73 | "DomUtils", 74 | "EJSON", 75 | "Ground", 76 | "GroundDB", 77 | "Kernel", 78 | "LocalCollection", 79 | "Meteor", 80 | "MiniMax", 81 | "Minimongo", 82 | "Npm", 83 | "OneTimeout", 84 | "OrderedDict", 85 | "Package", 86 | "Random", 87 | "SeededRandom", 88 | "ServerTime", 89 | "Store", 90 | "Tinytest", 91 | "Tracker", 92 | "_", 93 | "ReactiveVar" 94 | ], 95 | "globals": { 96 | "Ground": true, 97 | "GroundDB": true 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "dependencies": { 4 | "immediate": { 5 | "version": "3.0.6", 6 | "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", 7 | "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" 8 | }, 9 | "lie": { 10 | "version": "3.1.1", 11 | "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", 12 | "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==" 13 | }, 14 | "localforage": { 15 | "version": "1.9.0", 16 | "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.9.0.tgz", 17 | "integrity": "sha512-rR1oyNrKulpe+VM9cYmcFn6tsHuokyVHFaCM3+osEmxaHTbEk8oQu6eGDfS6DQLWi/N67XRmB8ECG37OES368g==" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: 4 | directories: 5 | - ~/.npm 6 | - ~/.meteor 7 | - "node_modules" 8 | 9 | node_js: 10 | - '8' 11 | 12 | install: 13 | - npm install 14 | - curl https://install.meteor.com/ | sh 15 | - export PATH=$HOME/.meteor:$PATH 16 | - npx meteor-ci login ${METEOR_TOKEN} --key ${METEOR_KEY} 17 | - meteor whoami 18 | 19 | stages: 20 | - test 21 | - release 22 | 23 | script: 24 | - npm test 25 | - if [ "$TRAVIS_BRANCH" == "master" ]; then npm run semantic-release; fi 26 | 27 | branches: 28 | except: 29 | - /^v\d+\.\d+\.\d+$/ 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## vCurrent 4 | ## [v0.3.5] (https://github.com/GroundMeteor/db/tree/v0.3.5) 5 | #### 02/02/15 by Morten Henriksen 6 | - Add clear() @jperl 7 | 8 | ## [v0.3.4] (https://github.com/GroundMeteor/db/tree/v0.3.4) 9 | #### 02/02/15 by Morten Henriksen 10 | - Make removeLocalOnly accessible - thanks @jperl 11 | 12 | ## [v0.3.3] (https://github.com/GroundMeteor/db/tree/v0.3.3) 13 | #### 01/02/15 by Morten Henriksen 14 | ## [v0.3.2] (https://github.com/GroundMeteor/db/tree/v0.3.2) 15 | #### 31/01/15 by Morten Henriksen 16 | - Bump to version 0.3.2 17 | 18 | - Add option disabling auto clean up of local only data 19 | 20 | - *Merged pull-request:* "Fix typo in README.md" [#74](https://github.com/GroundMeteor/db/issues/74) ([theneva](https://github.com/theneva)) 21 | 22 | - Fix typo in README.md 23 | 24 | Patches by GitHub user [@theneva](https://github.com/theneva). 25 | 26 | ## [v0.3.1] (https://github.com/GroundMeteor/db/tree/v0.3.1) 27 | #### 22/12/14 by Morten Henriksen 28 | 29 | - bump method storage 30 | 31 | ## [v0.3.0] (https://github.com/GroundMeteor/db/tree/v0.3.0) 32 | #### 21/12/14 by Morten Henriksen 33 | - Add resumed database event 34 | 35 | ## [v0.2.5] (https://github.com/GroundMeteor/db/tree/v0.2.5) 36 | #### 20/12/14 by Morten Henriksen 37 | - add changelog 38 | 39 | - Bump to version 0.2.5 40 | 41 | - *Fixed bug:* "Error "First argument to new Mongo.Collection..."" [#67](https://github.com/GroundMeteor/db/issues/67) 42 | 43 | ## [v0.2.4] (https://github.com/GroundMeteor/db/tree/v0.2.4) 44 | #### 17/12/14 by Morten Henriksen 45 | ## [v0.2.3] (https://github.com/GroundMeteor/db/tree/v0.2.3) 46 | #### 17/12/14 by Morten Henriksen 47 | - Bump to version 0.2.3 48 | 49 | - mbr update, remove versions.json 50 | 51 | ## [v0.2.2] (https://github.com/GroundMeteor/db/tree/v0.2.2) 52 | #### 17/12/14 by Morten Henriksen 53 | ## [v0.2.1] (https://github.com/GroundMeteor/db/tree/v0.2.1) 54 | #### 17/12/14 by Morten Henriksen 55 | - mbr update versions and fix warnings 56 | 57 | ## [v0.2.0] (https://github.com/GroundMeteor/db/tree/v0.2.0) 58 | #### 15/12/14 by Morten Henriksen 59 | - Use a basic dictionary to save more space in local storage 60 | 61 | - bump localstorage + 0.1.7 62 | 63 | - bump local storage using ejson 0.1.5 64 | 65 | - fix documentation 66 | 67 | - bump to 0.1.4 68 | 69 | - Arguments the events emitted 70 | 71 | - Add lookup for grounded collections 72 | 73 | - fix bug after last refactor 74 | 75 | - *Fixed bug:* "ReferenceError: GroundDB is not defined" [#64](https://github.com/GroundMeteor/db/issues/64) 76 | 77 | ## [Meteor-0-9-1] (https://github.com/GroundMeteor/db/tree/Meteor-0-9-1) 78 | #### 07/12/14 by Morten Henriksen 79 | - *Merged pull-request:* "Comment console.log in methodResume" [#62](https://github.com/GroundMeteor/db/issues/62) ([francocatena](https://github.com/francocatena)) 80 | 81 | - Comment console.log in methodResume 82 | 83 | - use latest OneTimeout 84 | 85 | - rewrite the method resume 86 | 87 | - Make a more clean method hack 88 | 89 | - Be more precise when dealing with methods 90 | 91 | - Make Ground.isResumed reactive 92 | 93 | - deprecate skipMethods in favor of a more precise Ground.methodResume 94 | 95 | - remove mrt legacy 96 | 97 | - update version and only support >1.0 98 | 99 | - Change scope a bit GroundDB -> Ground.Collection 100 | 101 | - update branch info 102 | 103 | - Try adding check meteor deps 104 | 105 | - add docs 106 | 107 | - more thoughts on conflict resolution 108 | 109 | - add comments about conflict handling 110 | 111 | - remove donate - folks can contact me directly 112 | 113 | - *Implemented enhancement:* "GroundDB is not an instance of Meteor/Mongo.Collection" [#47](https://github.com/GroundMeteor/db/issues/47) 114 | 115 | 116 | - make sure serve gets id on resume 117 | 118 | - use the _groundUtil api 119 | 120 | - *Fixed bug:* "No id returned by insert callback" [#48](https://github.com/GroundMeteor/db/issues/48) 121 | - *Fixed bug:* "the id is not passed to the insert callback" [#27](https://github.com/GroundMeteor/db/issues/27) 122 | 123 | - add more qa test interface 124 | 125 | - throw warning if used without the new keyword 126 | 127 | - Clean up and added testbed 128 | 129 | - major refactoring and bug hunt 130 | 131 | Patches by GitHub user [@francocatena](https://github.com/francocatena). 132 | 133 | ## [v0.1.4] (https://github.com/GroundMeteor/db/tree/v0.1.4) 134 | #### 29/08/14 by Morten Henriksen 135 | ## [pre-mps] (https://github.com/GroundMeteor/db/tree/pre-mps) 136 | #### 29/08/14 by Morten Henriksen 137 | ## [v0.1.3] (https://github.com/GroundMeteor/db/tree/v0.1.3) 138 | #### 29/08/14 by Morten Henriksen 139 | ## [v0.1.2] (https://github.com/GroundMeteor/db/tree/v0.1.2) 140 | #### 29/08/14 by Morten Henriksen 141 | ## [v0.1.1] (https://github.com/GroundMeteor/db/tree/v0.1.1) 142 | #### 29/08/14 by Morten Henriksen 143 | ## [v0.1.0] (https://github.com/GroundMeteor/db/tree/v0.1.0) 144 | #### 28/08/14 by Morten Henriksen 145 | ## [v0.0.22] (https://github.com/GroundMeteor/db/tree/v0.0.22) 146 | #### 22/08/14 by Morten Henriksen 147 | - *Merged pull-request:* "Fix for #36: loadMethods is crashing in v0.0.21" [#38](https://github.com/GroundMeteor/db/issues/38) ([waeltken](https://github.com/waeltken)) 148 | 149 | - Apply @jakobdamjensen 's fix for save- and loadObject methods. 150 | 151 | Patches by GitHub user [@waeltken](https://github.com/waeltken). 152 | 153 | ## [v0.0.21] (https://github.com/GroundMeteor/db/tree/v0.0.21) 154 | #### 23/04/14 by Morten Henriksen 155 | - *Merged pull-request:* "Fix for meteor 0.7.1.1 release and up" [#25](https://github.com/GroundMeteor/db/issues/25) ([Lauricio](https://github.com/Lauricio)) 156 | 157 | - Fix for meteor 0.7.1.1 release 158 | 159 | - *Merged pull-request:* "Update README.md" [#21](https://github.com/GroundMeteor/db/issues/21) ([DenisGorbachev](https://github.com/DenisGorbachev)) 160 | 161 | - *Merged pull-request:* "Update README.md" [#20](https://github.com/GroundMeteor/db/issues/20) ([DenisGorbachev](https://github.com/DenisGorbachev)) 162 | 163 | - *Merged pull-request:* "Update README.md" [#19](https://github.com/GroundMeteor/db/issues/19) ([DenisGorbachev](https://github.com/DenisGorbachev)) 164 | 165 | Patches by GitHub users [@Lauricio](https://github.com/Lauricio), [@DenisGorbachev](https://github.com/DenisGorbachev). 166 | 167 | ## [v0.0.20] (https://github.com/GroundMeteor/db/tree/v0.0.20) 168 | #### 06/12/13 by Morten Henriksen 169 | - *Merged pull-request:* "Added dependency, removed mac trash, version bump" [#17](https://github.com/GroundMeteor/db/issues/17) ([Lepozepo](https://github.com/Lepozepo)) 170 | 171 | - General: Updated git paths 172 | 173 | - Dependencies: Added ejson-minimax as an autoinstalled dependency 174 | 175 | - General: Ignore mac trash 176 | 177 | - Add MIT License 178 | 179 | - Edit package 180 | 181 | - Updated the flush methods to respect skip methods and made removeLocalOnly public 182 | 183 | - added minimax deps 184 | 185 | - add travis 186 | 187 | Patches by GitHub user [@Lepozepo](https://github.com/Lepozepo). 188 | 189 | ## [v0.0.19] (https://github.com/GroundMeteor/db/tree/v0.0.19) 190 | #### 06/09/13 by Morten Henriksen 191 | - *Merged pull-request:* "Renaming package name from GroundDB to grounddb" [#13](https://github.com/GroundMeteor/db/issues/13) ([merunga](https://github.com/merunga)) 192 | 193 | - renaming package to complete lowercase 194 | 195 | - Updating to GroundDB on_test 196 | 197 | - Fixed an issue updating tabs 198 | 199 | - Use each instead of Object.keys 200 | 201 | - Fixed a tiny bug that triggered Android 2.3.5 to crash 202 | 203 | Patches by GitHub user [@merunga](https://github.com/merunga). 204 | 205 | ## [v0.0.18] (https://github.com/GroundMeteor/db/tree/v0.0.18) 206 | #### 02/09/13 by Morten Henriksen 207 | - Corrected the local only tracker 208 | 209 | ## [v0.0.17] (https://github.com/GroundMeteor/db/tree/v0.0.17) 210 | #### 19/08/13 by Morten Henriksen 211 | ## [v0.0.16] (https://github.com/GroundMeteor/db/tree/v0.0.16) 212 | #### 29/07/13 by Morten Henriksen 213 | - Added extra validation of the offline database and GroundDB.ready for subscriptions status 214 | 215 | ## [v0.0.15] (https://github.com/GroundMeteor/db/tree/v0.0.15) 216 | #### 28/07/13 by Morten Henriksen 217 | ## [devel] (https://github.com/GroundMeteor/db/tree/devel) 218 | #### 28/07/13 by Morten Henriksen 219 | - Added `EJSON.minify` and `EJSON.maxify` 220 | 221 | - Added tests and EJSON.minify + EJSON.maxify 222 | 223 | - refactored preparing test writing 224 | 225 | - optimizing and bug hunting 226 | 227 | - Added documentation for subscriptions 228 | 229 | - Working on getting timestamps added to methods 230 | 231 | - Added serverTime + optimized for less access to localstorage 232 | 233 | ## [v0.0.14] (https://github.com/GroundMeteor/db/tree/v0.0.14) 234 | #### 26/07/13 by Morten Henriksen 235 | ## [v0.0.13] (https://github.com/GroundMeteor/db/tree/v0.0.13) 236 | #### 26/07/13 by Morten Henriksen 237 | ## [v0.0.12] (https://github.com/GroundMeteor/db/tree/v0.0.12) 238 | #### 26/07/13 by Morten Henriksen 239 | - Added write optimizations and updated docs 240 | 241 | - Added tab support for pure client-side offline databases 242 | 243 | ## [v0.0.11] (https://github.com/GroundMeteor/db/tree/v0.0.11) 244 | #### 25/07/13 by Morten Henriksen 245 | - *Merged pull-request:* "fix for #7" [#8](https://github.com/GroundMeteor/db/issues/8) ([chandika](https://github.com/chandika)) 246 | 247 | - *Fixed bug:* "Duplicate ID on initializing Meteor.users" [#7](https://github.com/GroundMeteor/db/issues/7) 248 | 249 | Patches by GitHub user [@chandika](https://github.com/chandika). 250 | 251 | ## [v0.0.10] (https://github.com/GroundMeteor/db/tree/v0.0.10) 252 | #### 25/07/13 by Morten Henriksen 253 | - *Fixed bug:* "Duplicate ID on initializing Meteor.users" [#7](https://github.com/GroundMeteor/db/issues/7) 254 | 255 | ## [v0.0.9] (https://github.com/GroundMeteor/db/tree/v0.0.9) 256 | #### 25/07/13 by Morten Henriksen 257 | ## [v0.0.8] (https://github.com/GroundMeteor/db/tree/v0.0.8) 258 | #### 25/07/13 by Morten Henriksen 259 | - Added support for smartCollection 260 | 261 | - Added resume and tab support for SmartCollections - untested 262 | 263 | - *Merged pull-request:* "a bit of formatting for nicely showing the API" [#5](https://github.com/GroundMeteor/db/issues/5) ([arunoda](https://github.com/arunoda)) 264 | 265 | - a bit of formatting for nicely showing the API 266 | 267 | Patches by GitHub user [@arunoda](https://github.com/arunoda). 268 | 269 | ## [v0.0.7] (https://github.com/GroundMeteor/db/tree/v0.0.7) 270 | #### 25/07/13 by Morten Henriksen 271 | - *Fixed bug:* "GroundDB not defined error" [#2](https://github.com/GroundMeteor/db/issues/2) 272 | 273 | - *Fixed bug:* "GroundDB not defined error" [#2](https://github.com/GroundMeteor/db/issues/2) 274 | 275 | - Removed unused file localstorage.adapter.js 276 | 277 | - Tested and working on IE9 - out of the box 278 | 279 | ## [v0.0.6] (https://github.com/GroundMeteor/db/tree/v0.0.6) 280 | #### 24/07/13 by Morten Henriksen 281 | ## [v0.0.5] (https://github.com/GroundMeteor/db/tree/v0.0.5) 282 | #### 24/07/13 by Morten Henriksen 283 | ## [v0.0.4] (https://github.com/GroundMeteor/db/tree/v0.0.4) 284 | #### 24/07/13 by Morten Henriksen 285 | ## [v0.0.3] (https://github.com/GroundMeteor/db/tree/v0.0.3) 286 | #### 24/07/13 by Morten Henriksen 287 | - Add console.log polyfill 288 | 289 | - Added link to online test 290 | 291 | ## [v0.0.2] (https://github.com/GroundMeteor/db/tree/v0.0.2) 292 | #### 23/07/13 by Morten Henriksen 293 | - Testing accounts and improving code 294 | 295 | - Clean up and use inheritance to save code 296 | 297 | ## [v0.0.1] (https://github.com/GroundMeteor/db/tree/v0.0.1) 298 | #### 22/07/13 by Morten Henriksen 299 | -------------------------------------------------------------------------------- /EVENTS.md: -------------------------------------------------------------------------------- 1 | Events in Ground DB II 2 | ====================== 3 | 4 | Events in Ground DB is emitted via a regular event emitter and the Meteor Tracker. 5 | 6 | ### Event / State 7 | 8 | Ground DB emits events and states, the event state `loaded` happens only 9 | once - but since it's a state `db.once('loaded');` will call back immediately if the 10 | event has already been emitted. 11 | 12 | * "loaded" -> `{ count }` 13 | 14 | ### Reactive variables 15 | 16 | To get info about the read / write state Ground DB currently uses Tracker to emit 17 | reactive events for progress. 18 | 19 | * `db.pendingWrites` 20 | * `db.pendingReads` 21 | 22 | Api for the read/write states 23 | * `progress()` -> `{ index, total, percent }` 24 | * `isDone()` -> `Boolean` 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 [@raix](https://github.com/raix), aka Morten N.O. Nørgaard Henriksen, mh@gi-software.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ground:db 2 | ========== 3 | 4 | [![Build Status](https://travis-ci.org/GroundMeteor/db.png?branch=grounddb-caching-2016)](https://travis-ci.org/GroundMeteor/db) 5 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 6 | 7 | GroundDB is a fast and thin layer providing Meteor offline database - Taking cloud data to the ground. 8 | 9 | ## Features 10 | This version of GroundDB is a caching only storage - meaning it does not support resuming of method calls/cross tab updates etc. But it's faster and async supporting local storages like: 11 | * localstorage 12 | * indexeddb 13 | * websql 14 | ~* SQLite (on cordova)~ 15 | 16 | *It's using localforage with some minor modifications - hopefully we can use localForage via npm in the future* 17 | 18 | [Notes about migration to GroundDB II](https://github.com/GroundMeteor/db/issues/153#issuecomment-206125703) 19 | 20 | ## Usage 21 | 22 | A pure offline collection 23 | ```js 24 | foo = new Ground.Collection('test'); 25 | ``` 26 | *Ground.Collection is client-side only and depends on `LocalCollection` for now* 27 | 28 | 29 | Get documents and updates from a Meteor Mongo Collection via DDP 30 | ```js 31 | foo = new Ground.Collection('test'); 32 | 33 | foo.observeSource(bar.find()); 34 | 35 | Meteor.setTimeout(() => { 36 | // Stop observing - keeping all documents as is 37 | foo.stopObserver(); 38 | }, 1000); 39 | ``` 40 | 41 | ## Limiting the stored data 42 | 43 | If you want to clean up the storage and eg. have it match the current subscription, now you can: 44 | ```js 45 | foo.keep(bar.find()); 46 | ``` 47 | *This will discard all documents not in the subscribed data* 48 | 49 | 50 | Limit the data stored locally 51 | ```js 52 | foo.keep(bar.find({}, { limit: 30 })); 53 | ``` 54 | *This will discard all but 30 documents* 55 | 56 | 57 | Limit the data stored locall using multiple cursors 58 | ```js 59 | foo.keep(bar.find({ type: 'a' }, { limit: 30 }), bar.find({ type: 'b' }, { limit: 30 })); 60 | ``` 61 | *This will keep at max 60 documents 30 documents of each type "a"/"b"* 62 | 63 | 64 | ## Clear the storage 65 | ```js 66 | foo.clear(); 67 | ``` 68 | *This will empty the in memory and the local storage* 69 | 70 | 71 | ## Need a near backwards compatible solution? 72 | 73 | This example behaves much like the previous version of ground db regarding caching a `Mongo.Collection` - This class inforces a manual clean up. Calling `removeLocalOnly()` will keep only the documents in the `Mongo.Collection`. 74 | 75 | ```js 76 | GroundLegacy = { 77 | Collection: class GroundLegacy extends Ground.Collection { 78 | constructor(collection, options) { 79 | if (!(collection instanceof Mongo.Collection)) { 80 | throw new Error('GroundLegacy requires a Mongo.Collection'); 81 | } 82 | if (options.cleanupLocalData !== false) { 83 | throw new Error('GroundLegacy requires cleanupLocalData to be false'); 84 | } 85 | 86 | // Create an instance of ground db 87 | super(collection._name); 88 | 89 | this.mongoCollection = collection; 90 | 91 | collection.grounddb = this; 92 | 93 | // Observe on the whole collection 94 | this.observeSource(collection.find()); 95 | 96 | // Store super 97 | collection.orgFind = collection.find; 98 | collection.orgFindOne = collection.findOne; 99 | 100 | // Overwrite collection finds using the grounded data 101 | collection.find = (...args) => { 102 | return this.find(...args); 103 | }; 104 | 105 | collection.findOne = (...args) => { 106 | return this.findOne(...args); 107 | }; 108 | } 109 | 110 | removeLocalOnly() { 111 | // Remove all documents not in current subscription 112 | this.keep(this.mongoCollection.orgFind()); 113 | } 114 | }, 115 | }; 116 | ``` 117 | 118 | ## More 119 | 120 | Read about: 121 | * [Events](EVENTS.md) in Ground DB 122 | 123 | ## Contributions 124 | Feel free to send issues, pull requests all is wellcome 125 | 126 | Kind regards Morten 127 | -------------------------------------------------------------------------------- /groundDB.client.tests.js: -------------------------------------------------------------------------------- 1 | // Tinytest.add('GroundDB - test environment', function(test) { 2 | 3 | // var a = new Ground.Collection('test', { 4 | // prefix: 'foobar', 5 | // connection: null 6 | // }); 7 | 8 | // }); 9 | 10 | 11 | 12 | 13 | // Tinytest.add('GroundDB - test storage', function(test) { 14 | // // Basic test 15 | // _gDB.storage.setItem('test', 'test-value'); 16 | // var val = _gDB.storage.getItem('test'); 17 | // // 1 18 | // test.equal(val, 'test-value'); 19 | // _gDB.storage.removeItem('test'); 20 | // val = _gDB.storage.getItem('test'); 21 | // // 2 22 | // test.equal(val, null); 23 | 24 | // // set test prefix 25 | // _gDB._prefix = 'test.'; 26 | // // 3 27 | // test.equal(_gDB._getGroundDBPrefix('suffix'), 'test.suffix', '_getGroundDBPrefix error'); 28 | 29 | // _gDB._prefix = 'groundDB.'; 30 | 31 | // var testSaveLoad = function(suffix, obj, text) { 32 | // _gDB._saveObject('suffix', obj); 33 | // var ret = _gDB._loadObject('suffix'); 34 | // test.isTrue(equals(obj, ret), text); 35 | // }; 36 | 37 | // // Test Objects IO 38 | // testSaveLoad('suffix', 1, 'Error testing '); 39 | // testSaveLoad('suffix', 'Hello', 'Error testing '); 40 | // testSaveLoad('suffix', { 3: 'Test' }, 'Error testing '); 41 | // testSaveLoad('suffix', [1, 2, 3], 'Error testing '); 42 | // testSaveLoad('suffix', -1, 'Error testing '); 43 | 44 | // }); 45 | 46 | // Tinytest.addAsync('GroundDB - Test local storage', function(test, completed) { 47 | // localStorage.clear(); 48 | 49 | // var foo = new GroundDB('test_foo', { 50 | // connection: null 51 | // }); 52 | 53 | // // Empty test collection 54 | // foo.find().forEach(function(doc) { 55 | // foo.remove({ _id: doc._id }); 56 | // }); 57 | 58 | // var local; 59 | 60 | // foo.insert({ foo: 'bar' }); 61 | 62 | // var item = foo.findOne(); 63 | 64 | // test.isTrue(!!item, 'No documents found...'); 65 | // if (item) { 66 | // test.equal(item.foo, 'bar', 'Invalid document found'); 67 | // } 68 | 69 | 70 | // Meteor.setTimeout(function() { 71 | 72 | // var name = 'db.test_foo'; 73 | 74 | // console.log('Load:', name); 75 | 76 | // local = _gDB._loadObject(name); 77 | 78 | // var keys = Object.keys(local); 79 | // var id = keys[0]; 80 | 81 | // console.log('Document in localStorage:', local[id]); 82 | 83 | // test.isTrue(!!local[id], 'No documents found...'); 84 | // if (local[id]) { 85 | // test.equal(local[id].foo, 'bar', 'Invalid document found'); 86 | // } 87 | 88 | // completed(); 89 | 90 | // }, 300); // 150 91 | // }); 92 | 93 | // Tinytest.addAsync('GroundDB - Test prefixed local storage', function(test, completed) { 94 | // localStorage.clear(); 95 | 96 | // var foo = new GroundDB('test_foo', { 97 | // connection: null, 98 | // prefix: 'test.' 99 | // }); 100 | 101 | // // Empty test collection 102 | // foo.find().forEach(function(doc) { 103 | // foo.remove({ _id: doc._id }); 104 | // }); 105 | 106 | // var local; 107 | 108 | // foo.insert({ foo: 'bar' }); 109 | 110 | // var item = foo.findOne(); 111 | 112 | // test.isTrue(!!item, 'No documents found...'); 113 | // if (item) { 114 | // test.equal(item.foo, 'bar', 'Invalid document found'); 115 | // } 116 | 117 | 118 | // Meteor.setTimeout(function() { 119 | 120 | // var name = 'db.test.test_foo'; 121 | 122 | // console.log('Load:', name); 123 | 124 | // local = _gDB._loadObject(name); 125 | 126 | // var keys = Object.keys(local); 127 | // var id = keys[0]; 128 | 129 | // console.log('Document in localStorage:', local[id]); 130 | 131 | // test.isTrue(!!local[id], 'No documents found...'); 132 | // if (local[id]) { 133 | // test.equal(local[id].foo, 'bar', 'Invalid document found'); 134 | // } 135 | 136 | // completed(); 137 | 138 | // }, 300); // 150 139 | // }); 140 | 141 | 142 | // Tinytest.add('GroundDB - Array.isArray', function(test) { 143 | // // all following calls return true 144 | // test.isTrue(Array.isArray([])); 145 | // test.isTrue(Array.isArray([1])); 146 | // test.isTrue(Array.isArray( new Array() )); 147 | // test.isTrue(Array.isArray( Array.prototype )); // Little known fact: Array.prototype itself is an array. 148 | 149 | // // all following calls return false 150 | // test.isFalse(Array.isArray()); 151 | // test.isFalse(Array.isArray({})); 152 | // test.isFalse(Array.isArray(null)); 153 | // test.isFalse(Array.isArray(undefined)); 154 | // test.isFalse(Array.isArray(17)); 155 | // test.isFalse(Array.isArray("Array")); 156 | // test.isFalse(Array.isArray(true)); 157 | // test.isFalse(Array.isArray(false)); 158 | // test.isFalse(Array.isArray({ __proto__ : Array.prototype })); 159 | 160 | // }); 161 | 162 | 163 | //Test API: 164 | //test.isFalse(v, msg) 165 | //test.isTrue(v, msg) 166 | //test.equalactual, expected, message, not 167 | //test.length(obj, len) 168 | //test.include(s, v) 169 | //test.isNaN(v, msg) 170 | //test.isUndefined(v, msg) 171 | //test.isNotNull 172 | //test.isNull 173 | //test.throws(func) 174 | //test.instanceOf(obj, klass) 175 | //test.notEqual(actual, expected, message) 176 | //test.runId() 177 | //test.exception(exception) 178 | //test.expect_fail() 179 | //test.ok(doc) 180 | //test.fail(doc) 181 | //test.equal(a, b, msg) 182 | -------------------------------------------------------------------------------- /groundDB.server.tests.js: -------------------------------------------------------------------------------- 1 | // function equals(a, b) { 2 | // return !!(JSON.stringify(a) === JSON.stringify(b)); 3 | // } 4 | 5 | // Tinytest.add('GroundDB - test', function(test) { 6 | 7 | // }); 8 | 9 | 10 | //Test API: 11 | //test.isFalse(v, msg) 12 | //test.isTrue(v, msg) 13 | //test.equalactual, expected, message, not 14 | //test.length(obj, len) 15 | //test.include(s, v) 16 | //test.isNaN(v, msg) 17 | //test.isUndefined(v, msg) 18 | //test.isNotNull 19 | //test.isNull 20 | //test.throws(func) 21 | //test.instanceOf(obj, klass) 22 | //test.notEqual(actual, expected, message) 23 | //test.runId() 24 | //test.exception(exception) 25 | //test.expect_fail() 26 | //test.ok(doc) 27 | //test.fail(doc) 28 | //test.equal(a, b, msg) 29 | -------------------------------------------------------------------------------- /lib/client/ground.db.js: -------------------------------------------------------------------------------- 1 | /* 2 | ______ ______ ____ 3 | / ____/________ __ ______ ____/ / __ \/ __ ) 4 | / / __/ ___/ __ \/ / / / __ \/ __ / / / / __ | 5 | / /_/ / / / /_/ / /_/ / / / / /_/ / /_/ / /_/ / 6 | \____/_/ \____/\__,_/_/ /_/\__,_/_____/_____/ 7 | 8 | 9 | GroundDB is a thin layer providing Meteor offline cached database 10 | 11 | When the app loads GroundDB resumes the cached database 12 | 13 | Regz. RaiX 14 | 15 | */ 16 | 17 | /* global Kernel: false */ // dispatch:kernel 18 | 19 | 20 | //////////////////////////////// GROUND DATABASE /////////////////////////////// 21 | Ground = {}; 22 | 23 | import localforage from 'localforage'; 24 | import { ProgressCount } from './pending.jobs'; 25 | import './servertime'; 26 | 27 | // Without the Kernel 28 | if (typeof Kernel === 'undefined') { 29 | var Kernel = { 30 | defer(f) { 31 | Meteor.setTimeout(f, 0); 32 | }, 33 | each(items, f) { 34 | items.forEach(f); 35 | }, 36 | }; 37 | } 38 | 39 | function strId(id) { 40 | if (id && id._str) { 41 | return id._str; 42 | } 43 | return id; 44 | } 45 | 46 | function throttle(func, timeFrame) { 47 | var lastTime = 0; 48 | return function () { 49 | var now = new Date(); 50 | if (now - lastTime >= timeFrame) { 51 | func(); 52 | lastTime = now; 53 | } 54 | }; 55 | } 56 | 57 | /* 58 | This function returns a throttled invalidation function binded on a collection 59 | */ 60 | const Invalidate = (collection, wait=100) => { 61 | return throttle(() => { 62 | Object.keys(collection._collection.queries) 63 | .forEach(qid => { 64 | const query = collection._collection.queries[qid]; 65 | if (query) { 66 | collection._collection._recomputeResults(query); 67 | } 68 | }); 69 | collection._collection._observeQueue.drain(); 70 | }, wait); 71 | }; 72 | 73 | // Global helper for applying grounddb on a collection 74 | Ground.Collection = class GroundCollection { 75 | 76 | constructor(name, { 77 | // Ground db options 78 | version=1.0, 79 | storageAdapter, 80 | throttle={}, 81 | supportRemovedAt=false, // Experimental, will remove documents with a "removedAt" stamp 82 | // Default Mongo.Collection options 83 | // xxx: not implemented yet 84 | // idGeneration='STRING', 85 | // transform, 86 | } = {}) { 87 | 88 | if (name !== ''+name || name === '') { 89 | throw new Meteor.Error('missing-name', 'Ground.Collection requires a collection name'); 90 | } 91 | 92 | this._collection = new LocalCollection(); 93 | 94 | this.throttle = Object.assign({ 95 | invalidate: 60, // Invalidations are throttled by 60ms 96 | }, throttle); 97 | 98 | // Use soft remove events to remove documents from the ground collection 99 | // Note: This feature is experimental 100 | this.supportRemovedAt = supportRemovedAt; 101 | 102 | // Is this an offline client only database? 103 | this.offlineDatabase = true; 104 | 105 | // Count for pending write operations 106 | this.pendingWrites = new ProgressCount(); 107 | 108 | // Count for pending read operations 109 | this.pendingReads = new ProgressCount(); 110 | 111 | // Carry last updated at if supported by schema 112 | this.lastUpdatedAt = null; 113 | 114 | this.isLoaded = false; 115 | 116 | this.pendingOnLoad = []; 117 | 118 | // Create scoped storage 119 | this.storage = storageAdapter || localforage.createInstance({ 120 | name: name, 121 | version: 1.0 // options.version 122 | }); 123 | 124 | // Create invalidator 125 | this.invalidate = Invalidate(this, this.throttle.invalidate); 126 | 127 | // Load database from local storage 128 | this.loadDatabase(); 129 | 130 | } 131 | 132 | loadDatabase() { 133 | // Then load the docs into minimongo 134 | this.pendingReads.inc(); 135 | this.storage 136 | .ready(() => { 137 | 138 | this.storage 139 | .length() 140 | .then(len => { 141 | if (len === 0) { 142 | this.pendingReads.dec(); 143 | Kernel.defer(() => { 144 | this.isLoaded = true; 145 | this.emitState('loaded', { count: len }); 146 | }); 147 | } else { 148 | // Update progress 149 | this.pendingReads.inc(len); 150 | // Count handled documents 151 | let handled = 0; 152 | this.storage 153 | .iterate((doc, id) => { 154 | Kernel.defer(() => { 155 | 156 | // Add the document to minimongo 157 | if (!this._collection._docs._map.has(id)) { 158 | this._collection._docs._map.set([id], EJSON.fromJSONValue(doc)); 159 | 160 | // Invalidate the observers pr. document 161 | // this call is throttled 162 | this.invalidate(); 163 | } 164 | 165 | // Update progress 166 | this.pendingReads.dec(); 167 | 168 | 169 | // Check if all documetns have been handled 170 | if (++handled === len) { 171 | Kernel.defer(() => { 172 | this.isLoaded = true; 173 | this.emitState('loaded', { count: len }); 174 | }); 175 | } 176 | }); 177 | 178 | }) 179 | .then(() => { 180 | this.pendingReads.dec(); 181 | }); 182 | } 183 | 184 | }); 185 | }); 186 | } 187 | 188 | runWhenLoaded(f) { 189 | if (this.isLoaded) { 190 | f(); 191 | } else { 192 | this.pendingOnLoad.push(f); 193 | } 194 | } 195 | 196 | saveDocument(doc, remove) { 197 | this.pendingWrites.inc(); 198 | doc._id = strId(doc._id); 199 | this.runWhenLoaded(() => { 200 | this.storage 201 | .ready(() => { 202 | 203 | if (remove) { 204 | this.storage 205 | .removeItem(doc._id) 206 | .then(() => { 207 | this.pendingWrites.dec(); 208 | }); 209 | } else { 210 | this.storage 211 | .setItem(doc._id, EJSON.toJSONValue(doc)) 212 | .then(() => { 213 | this.pendingWrites.dec(); 214 | }); 215 | } 216 | 217 | }); 218 | }); 219 | } 220 | 221 | setDocument(doc, remove) { 222 | doc._id = strId(doc._id); 223 | if (remove) { 224 | this._collection._docs._map.delete(doc._id); 225 | } else { 226 | this._collection._docs._map.set(doc._id, EJSON.clone(doc)); 227 | } 228 | this.invalidate(); 229 | } 230 | 231 | getLastUpdated(doc) { 232 | if (doc.updatedAt || doc.createdAt || doc.removedAt) { 233 | return new Date(Math.max(doc.updatedAt || null, doc.createdAt || null, doc.removedAt || null)); 234 | } 235 | 236 | return null; 237 | } 238 | 239 | setLastUpdated(lastUpdatedAt) { 240 | if (lastUpdatedAt && this.supportRemovedAt) { 241 | if (this.lastUpdatedAt < lastUpdatedAt || !this.lastUpdatedAt) { 242 | this.lastUpdatedAt = lastUpdatedAt || null; 243 | } 244 | } 245 | } 246 | 247 | stopObserver() { 248 | if (this.sourceHandle) { 249 | this.sourceHandle.stop(); 250 | this.sourceHandle = null; 251 | } 252 | } 253 | 254 | observeSource(source=this) { 255 | // Make sure to remove previous source handle if found 256 | this.stopObserver(); 257 | 258 | const cursor = (typeof (source||{}).observe === 'function') ? source : source.find(); 259 | // Store documents to localforage 260 | this.sourceHandle = cursor.observe({ 261 | 'added': doc => { 262 | this.setLastUpdated(this.getLastUpdated(doc)); 263 | if (this !== source) { 264 | this.setDocument(doc); 265 | } 266 | this.saveDocument(doc); 267 | }, 268 | // If removedAt is set this means the document should be removed 269 | 'changed': (doc, oldDoc) => { 270 | this.setLastUpdated(this.getLastUpdated(doc)); 271 | 272 | if (this.lastUpdatedAt) { 273 | if (doc.removedAt && !oldDoc.removedAt) { 274 | // Remove the document completely 275 | if (this !== source) { 276 | this.setDocument(doc, true); 277 | } 278 | this.saveDocument(doc, true); 279 | } else { 280 | if (this !== source) { 281 | this.setDocument(doc); 282 | } 283 | this.saveDocument(doc); 284 | } 285 | } else { 286 | if (this !== source) { 287 | this.setDocument(doc); 288 | } 289 | this.saveDocument(doc); 290 | } 291 | }, 292 | // If lastUpdated is supported by schema we should not use removed 293 | // any more - rather catch this in the changed event... 294 | 'removed': doc => { 295 | if (!this.lastUpdatedAt) { 296 | if (this !== source) { 297 | this.setDocument(doc, true); 298 | } 299 | this.saveDocument(doc, true); } 300 | } 301 | }); 302 | 303 | return { 304 | stop() { 305 | this.stopObserver(); 306 | } 307 | }; 308 | } 309 | 310 | shutdown(callback) { 311 | // xxx: have a better lock / fence 312 | this.writeFence = true; 313 | 314 | return new Promise(resolve => { 315 | Tracker.autorun(c => { 316 | // Wait until all writes have been done 317 | if (this.pendingWrites.isDone()) { 318 | c.stop(); 319 | 320 | if (typeof callback === 'function') { 321 | callback(); 322 | } 323 | resolve(); 324 | } 325 | }); 326 | }); 327 | } 328 | 329 | clear() { 330 | this.storage.clear(); 331 | this._collection._docs._map.clear() 332 | this.invalidate(); 333 | } 334 | 335 | 336 | /* 337 | Match the contents of the ground db to that of a cursor, or an array of cursors 338 | */ 339 | keep(cursors) { 340 | const arrayOfCursors = (Array.isArray(cursors)) ? cursors : [cursors]; 341 | // Map the ground db storage into an array of id's 342 | let currentIds = this._collection._docs._map.keys() 343 | currentIds = Array.from(currentIds); 344 | // Map each cursor id's into one flat array 345 | const keepIds = arrayOfCursors.map((cursor) => cursor.map((doc) => strId(doc._id))).flat(); 346 | // Remove all other documents from the collection 347 | const arrays = [currentIds, keepIds]; 348 | for (const id of arrays.reduce((a, b) => a.filter((c) => !b.includes(c)))) { 349 | // Remove it from in memory 350 | this._collection._docs._map.delete(id) 351 | // Remove it from storage 352 | this.saveDocument({ _id: id }, true); 353 | } 354 | 355 | this.invalidate(); 356 | } 357 | 358 | toJSON() { 359 | return JSON.stringify(Object.fromEntries(this._collection._docs._map.entries())) 360 | } 361 | 362 | // Simulate the Event Emitter Api "once" 363 | once(/* arguments */) { 364 | if (this.loaded) { 365 | Meteor.defer(arguments[1]); 366 | } else { 367 | this.pendingOnLoad.push(arguments[1]); 368 | } 369 | } 370 | 371 | // Simulate Event Emitter Api "emit" 372 | emitState(/* arguments */) { 373 | if (this.pendingOnLoad.length) { 374 | const pendingOnLoad = this.pendingOnLoad; 375 | this.pendingOnLoad = null; 376 | Meteor.defer(() => { 377 | pendingOnLoad.forEach(f => { 378 | f(); 379 | }) 380 | }) 381 | } 382 | } 383 | 384 | find(...args) { 385 | return this._collection.find(...args); 386 | } 387 | 388 | findOne(...args) { 389 | return this._collection.findOne(...args); 390 | } 391 | 392 | insert(...args) { 393 | const id = this._collection.insert(...args); 394 | this.saveDocument(this._collection.findOne(id)); 395 | return id; 396 | } 397 | 398 | upsert(selector, ...args) { 399 | const result = this._collection.upsert(selector, ...args); 400 | this.saveDocument(this._collection.findOne(selector)); 401 | return result; 402 | } 403 | 404 | update(selector, ...args) { 405 | const result = this._collection.upsert(selector, ...args); 406 | this.saveDocument(this._collection.findOne(selector)); 407 | return result; 408 | } 409 | 410 | remove(selector, ...args) { 411 | // Order of saveDocument and remove call is not important 412 | // when removing a document. (why we don't need carrier for the result) 413 | // only need the doc._id for removals 414 | const docs = this._collection.find(selector, {'fields': {'_id': 1}}).fetch(); 415 | for(let doc of docs) { 416 | this.saveDocument(doc, true); 417 | } 418 | return this._collection.remove(selector, ...args); 419 | } 420 | 421 | }; 422 | 423 | export default { Ground }; 424 | -------------------------------------------------------------------------------- /lib/client/pending.jobs.js: -------------------------------------------------------------------------------- 1 | export class ReactiveNumber extends ReactiveVar { 2 | inc(by=1) { 3 | this.curValue += by; 4 | this.dep.changed(); 5 | } 6 | dec(by=1) { 7 | this.inc(-by); 8 | } 9 | } 10 | 11 | /** 12 | * This class maintains a job counter, the total will 13 | * accumulate while increased and reset when pending hits 0 14 | * The api provides a `inc` and `dec` affecting the count. 15 | * 16 | * There are two reactive getters: 17 | * `progress` and `isDone` 18 | * 19 | */ 20 | export class ProgressCount { 21 | constructor() { 22 | this.count = new ReactiveNumber(0); 23 | this.total = new ReactiveNumber(0); 24 | this.percent = new ReactiveVar(0); 25 | this._depsDone = new Tracker.Dependency(); 26 | } 27 | 28 | calcPercent() { 29 | return Math.min(Math.round((this.total.curValue !== 0)? (1 - (this.count.curValue / this.total.curValue)) * 100 : 100), 100); 30 | } 31 | 32 | inc(by=1) { 33 | this.count.inc(by); 34 | this.total.inc(by); 35 | 36 | if (this.count.curValue === by) { 37 | this._depsDone.changed(); 38 | } 39 | 40 | this.percent.set(this.calcPercent()); 41 | } 42 | 43 | dec(by=1) { 44 | this.count.dec(by); 45 | if (this.count.curValue === 0) { 46 | // Reset thte total when count is 0 47 | this.total.set(0); 48 | this.count.set(0); 49 | 50 | this._depsDone.changed(); 51 | } 52 | this.percent.set(this.calcPercent()); 53 | } 54 | 55 | progress() { 56 | return { 57 | index: this.count.get(), 58 | total: this.total.get(), 59 | percent: this.percent.get() 60 | }; 61 | } 62 | 63 | isDone() { 64 | this._depsDone.depend(); 65 | return this.count.curValue === 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lib/client/servertime.js: -------------------------------------------------------------------------------- 1 | ////////////////////////// GET SERVER TIME DIFFERENCE ////////////////////////// 2 | import localforage from 'localforage'; 3 | 4 | ServerTime = {}; 5 | 6 | ServerTime._serverTimeDiff = 0; // Time difference in ms 7 | 8 | ServerTime.now = function() { 9 | return Date.now() + ServerTime._serverTimeDiff; 10 | }; 11 | 12 | // At server startup we figure out the time difference between server and 13 | // client time - this includes lag and timezone 14 | 15 | // Use the ground store to handle storage for us 16 | var _storage = localforage.createInstance({ 17 | name: 'ServerTime', 18 | version: 1.0, 19 | }); 20 | 21 | // Initialize the ServerTime._serverTimeDiff 22 | _storage.getItem('diff', function(err, time) { 23 | if (!err) { 24 | 25 | // Set the time 26 | ServerTime._serverTimeDiff = time || 0; 27 | } 28 | 29 | }); 30 | 31 | // Call the server method an get server time 32 | // XXX: Use http call instead creating less overhead 33 | Meteor.call('getServerTime', function(error, result) { 34 | if (!error) { 35 | // Update our server time diff 36 | ServerTime._serverTimeDiff = result - Date.now();// - lag or/and timezone 37 | // Update the localstorage 38 | _storage.setItem('diff', ServerTime._serverTimeDiff, function(/* err, result */) { 39 | // XXX: 40 | }); 41 | } 42 | }); // EO Server call 43 | 44 | export default ServerTime; 45 | -------------------------------------------------------------------------------- /lib/server/ground.db.js: -------------------------------------------------------------------------------- 1 | import ServerTime from './servertime'; 2 | 3 | Ground = {}; 4 | 5 | Ground.Collection = class GroundCollection { 6 | constructor(/* name, options = {} */) { 7 | throw new Error('Ground.Collection is client-side only'); 8 | } 9 | }; 10 | 11 | export default { Ground }; 12 | -------------------------------------------------------------------------------- /lib/server/servertime.js: -------------------------------------------------------------------------------- 1 | ////////////////////////// GET SERVER TIME DIFFERENCE ////////////////////////// 2 | 3 | ServerTime = {}; 4 | 5 | // XXX: TODO use a http rest point instead - creates less overhead 6 | Meteor.methods({ 7 | 'getServerTime': function() { 8 | return Date.now(); 9 | } 10 | }); 11 | 12 | // Unify client / server api 13 | ServerTime.now = function() { 14 | return Date.now(); 15 | }; 16 | 17 | export default ServerTime; 18 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "ground:db", 3 | version: "0.0.0-semantic-release", 4 | summary: "Ground Meteor.Collections offline", 5 | git: "https://github.com/GroundMeteor/db.git" 6 | }); 7 | 8 | Npm.depends({ 9 | localforage: '1.9.0', 10 | }); 11 | 12 | Package.onUse(function (api) { 13 | api.versionsFrom(['1.3', '2.3']); 14 | api.use(['ecmascript', 'mongo-id', 'reactive-var', 'diff-sequence', 'minimongo']); 15 | 16 | api.use([ 17 | 'ejson', 18 | ], ['client', 'server']); 19 | 20 | api.export('Ground'); 21 | 22 | api.use(['tracker'], 'client'); 23 | api.use(['dispatch:kernel@0.0.6'], 'client', { weak: true }); 24 | 25 | api.mainModule('lib/client/ground.db.js', 'client'); 26 | api.mainModule('lib/server/ground.db.js', 'server'); 27 | }); 28 | 29 | Package.onTest(function (api) { 30 | api.use('ground:db', ['client']); 31 | api.use('test-helpers', 'client'); 32 | api.use(['tinytest', 'underscore', 'ejson', 'ordered-dict', 33 | 'random', 'tracker']); 34 | 35 | api.addFiles('groundDB.client.tests.js', 'client'); 36 | api.addFiles('groundDB.server.tests.js', 'server'); 37 | }); 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db", 3 | "version": "0.0.0-semantic-release", 4 | "description": "Ground Meteor.Collections offline", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Tests not implemented\" && exit 0", 11 | "semantic-release": "semantic-release" 12 | }, 13 | "release": { 14 | "verifyConditions": [ 15 | "semantic-release-meteor", 16 | "@semantic-release/github" 17 | ], 18 | "getLastRelease": "semantic-release-meteor", 19 | "publish": [ 20 | "semantic-release-meteor", 21 | "@semantic-release/github" 22 | ] 23 | }, 24 | "repository": "https://github.com/GroundMeteor/db.git", 25 | "author": "", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/GroundMeteor/db/issues" 29 | }, 30 | "homepage": "https://github.com/GroundMeteor/db#readme", 31 | "devDependencies": { 32 | "meteor-ci": "0.3.1", 33 | "semantic-release": "^24.0.0", 34 | "semantic-release-meteor": "^0.0.7" 35 | } 36 | } 37 | --------------------------------------------------------------------------------