Package detail

gun-util

diatche908MIT0.0.18

Convenience and utility methods for GunDB

gun, gundb, gunjs, gun.js

readme

gun-util

Convenience and utility methods for GunDB.

Node.js CI

Tested with various Gun versions from 0.2019.930 up to GitHub master commit 5cbd5e3.

Note that this package is in early stages of development and there may be breaking changes within semantically compatible versions. See change log.

Installation

Node (CommonJS) and React (Native) (ESM)

  • Install using yarn with yarn add gun-util or npm npm install gun-util.
  • Install gun as a peer dependency. Supported versions:
    • 0.2019.930.
    • >0.2020.520.
    • GitHub master commit 7c45ddb or later.

Browser (UMD)

Include dependencies and the UMD bundle dist/index.umd.js. See an example here.

Documentation

Table of Contents:

  1. DateTree
  2. Encryption
  3. Auth
  4. Other Methods

DateTree

Efficiently distributes and stores data in a tree with nodes using date components as keys up to a specified resolution. The root of the tree is specified as a Gun node reference.

Example (up to hours):

         2020 (year)
        /  |  \
      04  ...  11 (month)
     /  \     /  \
    23  ..   ..   04 (day)
   /  \          /  \
  ..  ..        ..  .. (hour)

Why not just use a hash table?

Having large nodes is discouraged in a graph database like Gun. If you need to store large lists or tables of data, you need to break them up into smaller nodes to ease synchronization between peers.

Usage

For the exaples below, we will use the following setup:

import { DateTree } from 'gun-util';

let gun = Gun();
let treeRoot = gun.get('tree');
let tree = new DateTree(treeRoot, 'day');

Getting a node at a specific date:

Easily find a reference to a node using the date.

tree.get('2020-08-23').put({ event: 'of a lifetime' });
// The above is equivalent to:
treeRoot.get('2020').get('08').get('23').put({ event: 'of a lifetime' });
// This is assuming that UTC is the default time zone. See notes below.

Jump to Notes.

Subscribing to data:

// Subscribe to tree data with a filter
tree.on((data, date) => {
    console.log(`${date.toISOString()}: ${JSON.stringify(data)}`);
}, { gte: '2009-02-01' });

// Modify tree data
tree.get('1995-10-04T10:23:54.345Z').put('distant past');
tree.get('2010-04-05T15:34:17.234Z').put('past');
tree.get(new Date()).put('now');

// Output:
// 2010-04-05T15:34:17.234Z: "past"
// 2020-06-29T21:28:59.229Z: "now"

Getting latest data once:

await tree.get(new Date()).put({ event: 'insider info' }).then();
let [latestRef, date] = await tree.latest();
console.log(`Fetching latest event on ${date.toISOString()}...`);
console.log('event: ' + (await latestRef.then()).event);

// Output:
// Fetching latest event on 2020-06-18T00:00:00.000Z...
// event: insider info

Iterating through date references:

You can store data and iterate through only populated nodes without traversing all the nodes.

let tree = new DateTree(treeRoot, 'minute');

tree.get('1995-01-21 14:02').put({ event: 'good times' });
tree.get('2015-08-23 23:45').put({ event: 'ultimate' });
tree.get('2020-01-16 05:45').put({ event: 'earlybird' });

(async () => {
    // A naive implementation would have close to a billion
    // nodes and would take forever to iterate.
    // This takes only a second:
    for await (let [ref, date] of tree.iterate({ order: 1 })) {
        let event = await ref.get('event').then();
        console.log(`${date} event: ${event}`);
    }
    // Output:
    // Sat Jan 21 1995 14:02:00 GMT+0000 event: good times
    // Sun Aug 23 2015 23:45:00 GMT+0000 event: ultimate
    // Thu Jan 16 2020 05:45:00 GMT+0000 event: earlybird
})();

Filtering by date range and reverse iteration is possible using options:

tree.iterate({
    gte: '2020-01-01',
    lt: '2020-02-01',
    order: -1
})

Watch for changes:

Let's say we want to listen to changes to a blog described by a date tree.

How would we handle a case where we are close to the end of the nodes for the current time period?

For example, we are at 2019-12-31 23:54, which is the end of the hour, day, month and year. We may get a message this minute or next hour or day.

Subscribing to all nodes would be impractical.

Listen to a single path of the tree instead with changesAbout().

let tree = new DateTree(treeRoot, 'minute');

tree.get('1995-01-21 14:02').put({ blog: 'good times' });
tree.get('2015-08-23 23:45').put({ blog: 'ultimate' });
tree.get('2019-12-31 23:54').put({ blog: 'almost NY' });

let unsub = tree.changesAbout('2019-12-31 23:54', dateComponents => {
    /*
     * Whenever a node changes next to the direct path between the root and the
     * tree's maximum resolution, the callback is called with the date components
     * identifying the node. Note that the date components are partial unless the
     * change occured at the maximum resolution.
     */

    // Create a date to visualise the data
    let date = DateTree.getDateWithComponents(dateComponents);
    console.log(date.toISOString());
    // Output:
    // 1995-01-01T00:00:00.000Z
    // 2015-01-01T00:00:00.000Z
    // 2019-12-31T23:59:00.000Z
    // 2019-12-31T23:59:00.000Z
    // 2019-12-31T23:59:00.000Z
    // 2019-12-31T23:59:00.000Z
    // 2020-01-01T00:00:00.000Z
});

tree.get('2019-12-31 23:59').put({ blog: '3! 2! 1!' });
tree.get('2020-01-01 00:12').put({ blog: 'Happy NY!' });

At each date in the future, we can call tree.latest() and tree.iterate() to get the latest data.

When the date gets too far away, for example after 2020-01-01, we can call unsub() and resubscribe to a later date.

Other examples

Have a look at the examples folder.

Notes

  • The dates are stored in UTC time zone, but partial strings (without a time zone) are parsed in the local time zone (to be consistent with the convention). Avoid using partial string dates on trees with a resolution of day if this does not suit your use case. Using a resolution of hour solves most of the issues, but there are acouple of 30-minute time zones, so if you need perfection, then a resolution of minute is the way to go.
  • Noting the above, consider the scenario where we have a date tree with resolution day and the local time zone offset is +12:00:
    • If you use tree.get('2020-08-23'), you will in fact reference the previous date as the partial string parses into 2020-08-22T12:00:00.000Z.
      • Use tree.get('2020-08-23T00:00:00Z') or
      • tree.get(moment.utc('2020-08-23')) to avoid this.
    • Filtering with { gte: '2009-02-01' } will match dates after and including 2009-01-31.
      • Use { gte: '2009-02-01T00:00:00Z' } or
      • { gte: moment.utc('2009-02-01') } to avoid this.

Encryption

Allows encrypting and decrypting values and objects for the logged in user or for another user (using their epub key).

Encrypting private data:

Only the user can decrypt the value.

let pair = gun.user()._.sea;
let enc = await encrypt('a@a.com', { pair });
let dec = await decrypt(enc, { pair });
assert(dec === 'a@a.com');

Encrypting data for someone else:

Only the specified user can decrypt the value.

let frodo = await SEA.pair();
let gandalf = await SEA.pair();

let enc = await encrypt({ whereami: 'shire' }, {
    pair: frodo,
    recipient: gandalf // Or { epub: gandalf.epub }
});

let dec = await decrypt(enc, {
    pair: gandalf,
    sender: frodo // Or { epub: frodo.epub }
});

assert(dec.whereami === 'shire');

Other examples

Have a look at the examples folder.

Auth

Convenience methods for creating an authenticating a Gun user.

This wrapper provides greater consistency by handling a couple of edge cases, namely:

  • In some Gun versions, callbacks are not fired consistently. This wrapper listens to both local and global auth and returns in both cases.
  • When trying to log in to an existing user on an unsynced gun instance, you may get User does not exist! errors. This wrapper syncs the necessary data before attempting to login.
  • Another more troublesome pitfall with unsynced gun instances is if you try to create a user with an alias which has already been user on another Gun instance, you will get different key pairs and will not be able to sync the data of that user between those Gun instances. This wrapper takes extra precautions before creating a user (no guarantees though, it all depends on how long you want to wait for the sync to happen).

See also the Notes section below.

Basics:

let gun = new Gun()
let auth = new Auth(gun);

await auth.create({
    alias: 'alice',
    pass: 'secret'
});

auth.logout();

await auth.login({
    alias: 'alice',
    pass: 'secret'
});

Watching for authentication:

// With callback
auth.on(() => {
    let publicKey = auth.user();
    let pair = auth.pair();
});

// With promise
let publicKey = await auth.on();

Custom authentication recall:

auth.delegate = {
    storePair: async (pair, auth) => {
        await saveSecret(pair);
    },
    recallPair: async (auth, opts) => {
        await loadSecret(pair);
    },
}

Getting a user's public key with an alias:

let publicKey = await auth.getPub({ alias: 'alice' });

Have a look at the examples folder.

Notes

  • If you use your own gun.on('auth', cb) listener, call this.to.next(...args) inside of it to allow other listeners to receive a callback. Note that you will need to use the classic function declaration instead of an arrow function for correct this binding. You may also need prepend it with // @ts-ignore: if using TypeScript.
  • It's been observed that when Auth#exists(), Auth#getPub() or gun.get('~@' + alias) is used, gun.user().auth() stops working and fails immediately with an invalid credentials error.
    • Auth#login() uses gun.user().auth() with { wait: <timeout> } instead.
    • Avoid using Auth#exists(), Auth#getPub() and gun.get('~@' + alias) before logging in if this is an issue with your Gun version.

Other Methods

  • subscribe(ref, callback, opt?)

    • Subscribe to a Gun node ref and return a subscription.

      Unsubscribes automatically on uncaught errors inside the callback and rethrows.

      Why not just use ref.on()?

      Calling ref.off() unsubscribes all listeners, not just the last one. This method provides a way to unsubscribe only a single listener inline.

      For example:

      let dataRef = gun.get('data');
      let sub1 = subscribe(dataRef, data => console.log('sub1: ' + data));
      let sub2 = subscribe(dataRef, data => console.log('sub2: ' + data));
      dataRef.put('a');
      sub1.off();
      // sub2 is still active!
      dataRef.put('b');
      // Output:
      // sub1: a
      // sub2: a
      // sub2: b
  • waitForData(ref, { filter?, timeout? })
    • Returns a promise, which resolves when data arrives at a node reference.
  • delay(ms, passthrough?)
    • Promisified setTimeout().
  • errorAfter(ms, error)
    • Throw error after ms interval.
  • timeoutAfter(promise, ms, error?)
    • If the promise does not resolve (or error) within ms interval, throws a the specified error. If no error is specified, uses a TimeoutError instead.

Development

For faster unit tests, create an .env.local file in the project directory and add a test gun peer:

TEST_GUN_PEERS="https://<your-id>.herokuapp.com/gun"

Alternatively, use export TEST_GUN_PEERS="https://<your-id>.herokuapp.com/gun" before running unit tests.

changelog

Change Log

master

Changes on master will be listed here.

0.0.18

9 Jul 2022

Bug Fixes

  • [#34] auth.cb now correctly passes the public key in the callback.

0.0.17

30 May 2021

Other Changes

  • Updated dependencies.

0.0.16

10 Oct 2020

Features

  • Listening to authentication using Auth does not affect the previous gun.on('auth') listener.

Breaking Changes

  • Renamed enviroment variable GUN_PEERS to TEST_GUN_PEERS.

0.0.14 - 0.0.15

18 Aug 2020

Bug Fixes

  • Fixed UMD module error: Can't find variable: process.

Breaking Changes

  • Renamed UMD module global variable global['gun-util'] to GunUtil.

0.0.13

18 Aug 2020

Features

  • Added support for Gun v0.2019.930.

Breaking Changes

  • Removed isGunAuthPairSupported() as authenticating with a pair was in fact supported in Gun v0.2019.*.

0.0.12

14 Aug 2020

Features

  • Added subscribe() method to subscribe to on() events, returning a subscription object.

Bug Fixes

  • Fixed subscription bugs by only using off() inside of callbacks. See issue.

Breaking Changes

  • Renamed types:
    • Subscription to IGunSubscription.
    • ExpandedCallback to GunSubscriptionCallback.

0.0.11

Bug Fixes

  • Fixed Auth#login(), which has stopped working since 0.0.10 with new Gun instances.

0.0.10

Features

  • When attempting to start another login while another is in progress, Auth throws a MultipleAuthError instead of AuthError.
  • Added errorAfter() and timeoutAfter() utility methods.
  • Added Auth#did() method to allow using your own gun.on('auth', cb) listener.
  • You can pass a callback to Auth#on.
  • Added Auth#getPub(), which finds the public key corresponding to a user alias.
  • Added Auth#exists(), which checks if a user with an alias exists.
  • You can wait for a user operation to finish using Auth#join().
  • Added an optional timeout to waitForData().
  • Added closedFilter() which converts an open filter to a closed filter.

Bug Fixes

  • Fixed unsubscribe on promise cancel in waitForData() and delay().

Breaking Changes

  • Renamed Auth#onAuth() to Auth#on().
  • waitForData() now takes an options object as the second parameter containing filter instead of the filter itself.

0.0.9

Features

  • Made DateTree.parseDate() public.

Bug Fixes

  • Fixed using DateTree with non UTC dates.

Breaking Changes

  • Removed redundant method DateTree.iterateDates().

0.0.8

  • Updated dependencies.

0.0.7

Features

  • Added DateTree#on() method, which allows subscribing to all or a subset of the tree's data.
  • Filter methods support any value which has a valueOf() method.
  • DateTree#getDate() can now be used with a reference from a callback.
  • Added DateTree#largestCommonUnit().
  • Added DateTree#getDateComponentKeyRange().
  • Added DateTree#dateComponentsToString().

Breaking Changes

  • Renamed filterKey() to isInRange().

0.0.6

Features

  • waitForData allows waiting for data to arrive at a node reference.
  • Auth supports login with a SEA pair.
  • Auth supports user recall on web and via a delegate on other platforms.

Breaking Changes

  • The stateless GunUser has been replaced with a stateful Auth. The methods are almost identical, with the exception of GunUser.current(), which is now Auth#user().
  • IterateOptions no longer uses start, end, startInclusive, endInclusive. Instead gt, gte, lt, lte is used. These can be converted to a Range, which resemble the previous structure (which is still used internally) using rangeWithFilter().
  • Removed redundant DateTree#put(). Use DateTree#get().put() instead.
  • DateTree#changesAbout() returns an object instead of the off() function directly.

0.0.5

Features

  • Encryption methods.