├── .gitignore ├── LICENSE ├── README.md ├── bullet.js ├── examples ├── index.html ├── package-lock.json └── remove.html └── utilities ├── remove.js ├── rpc.js └── ttl.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | backup/ 4 | node_modules/ 5 | playground/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Levi Roberts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bullet 2 | Wrapper around [GunDB](https://github.com/amark/gun) to provide alternative syntax, promise support, utility modules, and easy Gun adapter events. 3 | 4 | # Rewriting Gun docs: # 5 | 6 | ## Original: ## 7 | 8 | var gun = Gun(); 9 | 10 | gun.get('mark').put({ 11 | name: "Mark", 12 | email: "mark@gunDB.io", 13 | }); 14 | 15 | gun.get('mark').on(function(data, key){ 16 | console.log("update:", data); 17 | }); 18 | 19 | ## With Bullet: ## 20 | 21 | var bullet = new Bullet() 22 | 23 | bullet.mark = { 24 | name: "Mark", 25 | email: "mark@gunDB.io", 26 | } 27 | 28 | bullet.mark.on(data => { 29 | console.log("update:", data); 30 | }) 31 | 32 | ## Second Example: ## 33 | 34 | var cat = {name: "Fluffy", species: "kitty"}; 35 | var mark = {boss: cat}; 36 | cat.slave = mark; 37 | 38 | // partial updates merge with existing data! 39 | gun.get('mark').put(mark); 40 | 41 | // access the data as if it is a document. 42 | gun.get('mark').get('boss').get('name').once(function(data, key){ 43 | // `val` grabs the data once, no subscriptions. 44 | console.log("Mark's boss is", data); 45 | }); 46 | 47 | // traverse a graph of circular references! 48 | gun.get('mark').get('boss').get('slave').once(function(data, key){ 49 | console.log("Mark is the slave!", data); 50 | }); 51 | 52 | // add both of them to a table! 53 | gun.get('list').set(gun.get('mark').get('boss')); 54 | gun.get('list').set(gun.get('mark')); 55 | 56 | // grab each item once from the table, continuously: 57 | gun.get('list').map().once(function(data, key){ 58 | console.log("Item:", data); 59 | }); 60 | 61 | // live update the table! 62 | gun.get('list').set({type: "cucumber", goal: "scare cat"}); 63 | 64 | ## With Bullet: ## 65 | 66 | var cat = { name: 'Fluffy', species: 'kitty' } 67 | var mark = { boss: cat } 68 | cat.slave = mark 69 | 70 | // partial updates merge with existing data! 71 | bullet.mark = mark 72 | 73 | // access the data as if it is a document. 74 | let marksBoss = await bullet.mark.boss.name.value 75 | console.log("Mark's boss is", marksBoss) 76 | 77 | // add both of them to a table! 78 | bullet.list.set(bullet.mark.boss) 79 | bullet.list.set(bullet.mark) 80 | 81 | // grab each item once from the table, continuously: 82 | bullet.list.map().once(data => { 83 | console.log("Item:", data) 84 | }) 85 | 86 | // live update the table! 87 | bullet.list.set({ type: "cucumber", goal: "scare cat" }) 88 | 89 | # How is this possible? # 90 | It's simple really. Bullet wraps the Gun instance with a Proxy. Each property lookup (using dot notation or bracket) calls a `gun.get()` and returns the Proxy to be used for chaining. Any call to `bullet.value` will return a promise getter of `gun.once()`. 91 | 92 | # Utilities: # 93 | - `.value` - Promise getter for `.once()`; `let cats = await bullet.cats.value` 94 | - `.remove()` - `bullet.cats.remove()` 95 | 96 | # Writing your own Gun adapter, made easy: # 97 | One of the best features of Bullet is to write Gun adapters without having to know too much about the Gun constructor, proper placement to initialize your adapter, and bugs caused by not forwarding the events. Bullet takes care of all of this for you! Simply define a `Function` or `Class`, return an object that contains the events you want to hook into, and the rest is up to you. 98 | 99 | Example class: 100 | 101 | class gunAdapter { 102 | constructor(bullet, opts, context) { 103 | return { 104 | events : { 105 | // Storage 106 | get: function(...), 107 | put: function(...), 108 | 109 | // Wire 110 | in: function(...), 111 | out: function(...), 112 | } 113 | } 114 | } 115 | } 116 | 117 | Example function: 118 | 119 | function gunAdapter(bullet, opts, context) { 120 | return { 121 | events: { 122 | // Storage 123 | get: function(...), 124 | put: function(...), 125 | 126 | // Wire 127 | in: function(...), 128 | out: function(...), 129 | } 130 | } 131 | } 132 | 133 | To use your adapter, include it in your project then:
134 | `bullet.extend(gunAdapter)` 135 | 136 | # More coming soon! # 137 | Bullet expects to be a wrapper for other utility functions, offering an easy way to extend either bullet or gun via Proxies or directly. This will allow you to create custom bullet methods for wrapping verbose syntaxes. 138 | 139 | Some utility proposals have already emerged, such as handling arrays in a more suitable fashion. 140 | 141 | bullet.cats.sparky = { color: 'orange' } 142 | bullet.cats.howie = { color: 'white' } 143 | 144 | bullet.mark.cats = [bullet.cats.sparky, bullet.cats.howie] 145 | 146 | Another utility proposal was RPC 147 | 148 | // local 149 | bullet.rpc.host('peerName') 150 | bullet.rpc.register('procName', function(data) {}) 151 | 152 | // Remote 153 | bullet.rpc.exec('procName', { some: 'data' }) 154 | // or 155 | bullet.rpc.select('peerName').exec('procName') 156 | 157 |
158 | 159 | ## License: ## 160 | [MIT](https://github.com/bugs181/bullet/blob/master/LICENSE) 161 | -------------------------------------------------------------------------------- /bullet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class Bullet { 4 | constructor(gun, opts) { 5 | this.gun = gun 6 | this.Gun = (typeof window !== 'undefined') ? window.Gun : require('gun/gun') 7 | 8 | // If we don't pass gun instance, then see if it's available from window or file - use Bullet's args to create a new gun instance. 9 | /*const hasGun = (gun && gun.chain) ? true : false 10 | if (!hasGun) 11 | this.gun = this.Gun(...arguments) 12 | */ 13 | 14 | this._ctx = null 15 | this._ctxVal = null 16 | this._ready = true 17 | this._proxyEnable = true 18 | 19 | // Immutability is an opt-in feature. use: new Bullet(gun, { immutable: true }) 20 | this.immutable = (opts && opts.immutable) ? true : false 21 | 22 | const that = this 23 | this.Gun.on('opt', function(context) { 24 | that._registerContext = context 25 | this.to.next(context) 26 | }) 27 | this.gun = this.Gun(...arguments) 28 | 29 | this.mutate = this.mutate.bind(this) 30 | this.extend = this.extend.bind(this) 31 | 32 | return new Proxy(this.gun, bulletProxy(this)) 33 | } 34 | 35 | get value() { 36 | return new Promise((resolve, reject) => { 37 | if (!this || !this._ctx || !this._ctx.once) 38 | return reject('No gun context') 39 | 40 | this._ctx.once(data => { 41 | let timer = setInterval(() => { 42 | // Wait until bullet is ready (pending .put results) 43 | if (this._ready) { 44 | resolve(data) 45 | clearInterval(timer) 46 | } 47 | }, 100) 48 | }) 49 | }) 50 | } 51 | 52 | get events() { 53 | // Example use: bullet.events.on('get') 54 | return this._registerContext 55 | } 56 | 57 | mutate(val) { 58 | if (!val && this._ctxVal) { 59 | this._ready = false 60 | this._ctxProp.put(this._ctxVal, () => this._ready = true) 61 | } 62 | } 63 | 64 | extend(clss, opts) { 65 | this._proxyEnable = false 66 | if (typeof cls === 'object') 67 | if (!Array.isArray(clss)) 68 | throw new Error('bullet.extends() only supports a single utility or an array of utilities') 69 | else 70 | clss = [clss] 71 | else 72 | clss = [clss] 73 | 74 | for (let cls of clss) 75 | if (typeof cls === 'function') { 76 | const instance = new cls(this, opts, this._registerContext) 77 | this[instance.name] = instance 78 | this._registerInstanceHooks(instance) 79 | } 80 | this._proxyEnable = true 81 | } 82 | 83 | _registerInstanceHooks(instance) { 84 | // Register Gun.opts.on() events 85 | if (typeof instance.events === 'object') 86 | for (let event of Object.keys(instance.events)) { 87 | if (typeof instance.events[event] === 'function') 88 | this._registerContext.on(event, instance.events[event]) 89 | } 90 | } 91 | } 92 | 93 | 94 | function bulletProxy(base) { 95 | return { 96 | get (target, prop, receiver) { 97 | // Return any class methods/props 98 | if (prop in target || prop === 'inspect' || prop === 'constructor' || typeof prop == 'symbol') { 99 | if (typeof target[prop] === 'function') 100 | target[prop] = target[prop].bind(target) 101 | 102 | return Reflect.get(target, prop, receiver) 103 | } 104 | 105 | // Proxy all other requests as chainables 106 | if (base[prop]) // Method exists in Bullet 107 | return base[prop] 108 | 109 | // Method does not exist, is a chainable 110 | base._ctx = new Proxy(target.get(prop), bulletProxy(base)) 111 | return base._ctx 112 | }, 113 | 114 | set (target, prop, receiver) { 115 | if (prop in base || !base._proxyEnable) 116 | return base[prop] = receiver 117 | 118 | if (!base.immutable) { 119 | this._ready = false 120 | target.get(prop).put(receiver, () => base._ready = true) 121 | } else { 122 | // eslint-disable-next-line no-console 123 | console.warn('You have immutable turned on; be sure to .mutate()') 124 | base._ctxProp = target.get(prop) 125 | base._ctxVal = receiver 126 | base._ready = true 127 | } 128 | 129 | return target 130 | }, 131 | } 132 | } 133 | 134 | // If environment is not browser, export it (for node compatibility) 135 | if (typeof window === 'undefined') 136 | module.exports = Bullet 137 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |

Try it out!

11 | 12 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "async-limiter": { 6 | "version": "1.0.0", 7 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", 8 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" 9 | }, 10 | "gun": { 11 | "version": "0.9.999998", 12 | "resolved": "https://registry.npmjs.org/gun/-/gun-0.9.999998.tgz", 13 | "integrity": "sha512-a2NIHxA2Tv6E+8t9BQ2fl8Jg2AL4DZRQiUwcNM93steraC/MLYLc4rh0t/lYFHNDELdMyndqYIPgdRa2OD86nw==", 14 | "requires": { 15 | "node-webcrypto-ossl": "^1.0.39", 16 | "text-encoding": "^0.7.0", 17 | "ws": "~>5.2.0" 18 | } 19 | }, 20 | "minimist": { 21 | "version": "0.0.8", 22 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 23 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 24 | }, 25 | "mkdirp": { 26 | "version": "0.5.1", 27 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 28 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 29 | "requires": { 30 | "minimist": "0.0.8" 31 | } 32 | }, 33 | "nan": { 34 | "version": "2.12.1", 35 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz", 36 | "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==" 37 | }, 38 | "node-webcrypto-ossl": { 39 | "version": "1.0.39", 40 | "resolved": "https://registry.npmjs.org/node-webcrypto-ossl/-/node-webcrypto-ossl-1.0.39.tgz", 41 | "integrity": "sha512-cEq67y6GJ5jcKdANi5XqejqMvM/eIGxuOOE8F+c0XS950jSpvOcjUNHLmIe3/dN/UKyUkb+dri0BU4OgmCJd2g==", 42 | "requires": { 43 | "mkdirp": "^0.5.1", 44 | "nan": "^2.11.1", 45 | "tslib": "^1.9.3", 46 | "webcrypto-core": "^0.1.25" 47 | } 48 | }, 49 | "text-encoding": { 50 | "version": "0.7.0", 51 | "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", 52 | "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==" 53 | }, 54 | "tslib": { 55 | "version": "1.9.3", 56 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", 57 | "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" 58 | }, 59 | "webcrypto-core": { 60 | "version": "0.1.26", 61 | "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-0.1.26.tgz", 62 | "integrity": "sha512-BZVgJZkkHyuz8loKvsaOKiBDXDpmMZf5xG4QAOlSeYdXlFUl9c1FRrVnAXcOdb4fTHMG+TRu81odJwwSfKnWTA==", 63 | "requires": { 64 | "tslib": "^1.7.1" 65 | } 66 | }, 67 | "ws": { 68 | "version": "5.2.2", 69 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 70 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 71 | "requires": { 72 | "async-limiter": "~1.0.0" 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/remove.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |

Try it out!

12 | 13 | 31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /utilities/remove.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class remove { 4 | constructor(bullet) { 5 | return function remove(maxDepth) { 6 | bullet._ctx.once(props => nullProps(bullet._ctx, props, maxDepth)) 7 | } 8 | 9 | function nullProps(context, obj, maxDepth, depth = 0) { 10 | if (!obj) 11 | return 12 | 13 | console.log('To null: ', obj) 14 | 15 | if (typeof obj === 'object') 16 | for (let key of Object.keys(obj)) { 17 | if (key === '_') 18 | continue 19 | 20 | if (typeof obj[key] === 'string') { 21 | console.log('nulling: ', key) 22 | context.get(key).put(null) 23 | } else { 24 | if (!obj[key]) 25 | continue 26 | 27 | console.log('Get: ', key) 28 | //console.log(depth) 29 | 30 | if (depth === 0) 31 | context = context.get(key) 32 | else 33 | context = context.back(depth).get(key) 34 | 35 | context.once(props => { nullProps(context, props, maxDepth, depth); depth++ }) 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | // If environment is not browser, export it (for node compatibility) 43 | if (typeof window === 'undefined') 44 | module.exports = remove 45 | 46 | // https://github.com/d3x0r/gun-unset/blob/master/unset.js 47 | // https://github.com/d3x0r/gun-db/blob/c189a9a5d5ae3ded5616e32cabc4e110e9cf6520/testsetnul.js#L67 48 | // https://github.com/amark/gun/issues/404 49 | 50 | /* 51 | if (key === '#') { 52 | console.log('Get: ', key) 53 | bullet._ctx.get(key).once(props => deleteProps(bullet, props)) 54 | } else 55 | */ 56 | -------------------------------------------------------------------------------- /utilities/rpc.js: -------------------------------------------------------------------------------- 1 | // Stub for RPC 2 | -------------------------------------------------------------------------------- /utilities/ttl.js: -------------------------------------------------------------------------------- 1 | // Stub for TTL 2 | --------------------------------------------------------------------------------