Repository: textileio/go-threads
Pull request #450 (2020-10-08 23:15:49)

Local first optimizations
This PR contains a few small nits and optimizations that make building on offline-first client for ThreadDB easier. The main changes are that a call to Save can now also lead to a Create action being generated if and only if the call to Save includes an existing Instance ID as a simple safety check. This allows callers to write or overwrite an existing Instance, without having to check first if it already exists. An additional change is that Delete now silently ignores missing Instances. This means a client can call delete on an Instance that might have already been deleted while it was offline or similar, without throwing an error.

Finally, the more important addition is the "special" _mod field. This is simple a "readonly" timestamp that gets added to all Instances. Any changes to this field will be ignored by the peer, and will be automatically updated on Save (and initialized on Create). This doesn't really have any effect on Delete, which is partly why the ModifiedSince experimental API was also created.


Patch:
diff --git a/db/README.md b/db/README.md
index 60d40d2..47f8548 100644
--- a/db/README.md
+++ b/db/README.md
@@ -98,7 +98,7 @@ with logical clocks.
 #### Dispatcher
 This is an internal component not available in the public API.
 Main responsibility: Source of truth regarding known `db.Event`s for the 
-`DB`. Will notify registered parties to let them know about new ones..
+`DB`. Will notify registered parties to let them know about new ones.
 
 Every `Event` generated in the `DB` is sent to a `Dispatcher` when write 
 transactions are committed. The dispatcher is responsible for broadcasting 
diff --git a/db/collection.go b/db/collection.go
index bb67700..c2a24f8 100644
--- a/db/collection.go
+++ b/db/collection.go
@@ -5,14 +5,17 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/alecthomas/jsonschema"
 	"github.com/dop251/goja"
 	jsonpatch "github.com/evanphx/json-patch"
-	"github.com/ipfs/go-ipld-format"
+	format "github.com/ipfs/go-ipld-format"
 	ds "github.com/textileio/go-datastore"
+	"github.com/textileio/go-datastore/query"
 	"github.com/textileio/go-threads/core/app"
 	core "github.com/textileio/go-threads/core/db"
 	"github.com/textileio/go-threads/core/thread"
@@ -36,9 +39,9 @@ var (
 	ErrInvalidSchemaInstance = errors.New("instance doesn't correspond to schema")
 
 	errMissingInstanceID           = errors.New("invalid instance: missing _id attribute")
+	errMissingModTag               = errors.New("invalid instance: missing _mod attribute")
 	errAlreadyDiscardedCommitedTxn = errors.New("can't commit discarded/committed txn")
 	errCantCreateExistingInstance  = errors.New("can't create already existing instance")
-	errCantSaveNonExistentInstance = errors.New("can't save unkown instance")
 
 	baseKey = dsPrefix.ChildString("collection")
 )
@@ -263,6 +266,68 @@ func (c *Collection) Find(q *Query, opts ...TxnOption) (instances [][]byte, err
 	return
 }
 
+type filter struct {
+	Collection string
+	Time       int
+}
+
+func (f filter) Filter(e query.Entry) bool {
+	// Easy out, are we in the right collection?
+	key := ds.NewKey(e.Key)
+	base := key.Parent().BaseNamespace()
+	number, _ := strconv.Atoi(base)
+	kind := key.Type()
+	return kind == f.Collection && number > f.Time
+
+}
+
+// ModifiedSince returns a list of all instances that have been modified (and/or touched) since `time`.
+func (c *Collection) ModifiedSince(time int64, opts ...TxnOption) (modified []core.InstanceID, err error) {
+	// Need to identify instances that have been touched (created, saved, or deleted) since `time`.
+	// The _mod field tracks modified instances, but not those that have been deleted, so we need
+	// to query the dispatcher for all (unique) instances in this collection that have been modified
+	// at all since `time`.
+	c.db.dispatcher.Lock().Lock()
+	defer c.db.dispatcher.Lock().Unlock()
+
+	txn, err := c.db.dispatcher.Store().NewTransaction(false)
+	if err != nil {
+		return nil, err
+	}
+	defer txn.Discard()
+
+	query := query.Query{
+		Prefix:     dsDispatcherPrefix.String(),
+		SeekPrefix: dsDispatcherPrefix.ChildString(strconv.FormatInt(time, 10)).String(),
+		Filters: []query.Filter{
+			filter{
+				Collection: c.name,
+			},
+		},
+		KeysOnly: true,
+	}
+	results, err := txn.Query(query)
+	if err != nil {
+		return nil, err
+	}
+
+	type void struct{}
+	var member void
+	set := make(map[core.InstanceID]void)
+
+	for r := range results.Next() {
+		if r.Error != nil {
+			return nil, r.Error
+		}
+		id := ds.NewKey(r.Key)
+		set[core.InstanceID(id.Name())] = member
+	}
+	for k := range set {
+		modified = append(modified, k)
+	}
+	return modified, nil
+}
+
 // validInstance validates the json object against the collection schema.
 func (c *Collection) validInstance(v []byte) error {
 	r, err := gojsonschema.Validate(c.schemaLoader, gojsonschema.NewBytesLoader(v))
@@ -322,9 +387,8 @@ func (c *Collection) validWrite(identity thread.PubKey, e core.Event) error {
 	case bool:
 		if out.(bool) {
 			return nil
-		} else {
-			return app.ErrInvalidNetRecordBody
 		}
+		return app.ErrInvalidNetRecordBody
 	case nil:
 		return app.ErrInvalidNetRecordBody
 	default:
@@ -407,6 +471,9 @@ func (t *Txn) Create(new ...[]byte) ([]core.InstanceID, error) {
 			return nil, errCantCreateExistingInstance
 		}
 
+		// Update readonly/protected mod tag
+		_, updated = setModifiedTag(updated)
+
 		a := core.Action{
 			Type:           core.Create,
 			InstanceID:     id,
@@ -455,6 +522,7 @@ func (t *Txn) Save(updated ...[]byte) error {
 	if err != nil {
 		return err
 	}
+
 	t.actions = append(t.actions, actions...)
 	return nil
 }
@@ -473,6 +541,11 @@ func (t *Txn) createSaveActions(identity thread.PubKey, updated ...[]byte) ([]co
 			return nil, err
 		}
 
+		// Update readonly/protected mod tag
+		_, next = setModifiedTag(next)
+
+		// Because this is a save event, even though we might still create the new instance
+		// it has to have a valid _id ahead of time.
 		id, err := getInstanceID(next)
 		if err != nil {
 			return nil, err
@@ -480,7 +553,16 @@ func (t *Txn) createSaveActions(identity thread.PubKey, updated ...[]byte) ([]co
 		key := baseKey.ChildString(t.collection.name).ChildString(id.String())
 		previous, err := t.collection.db.datastore.Get(key)
 		if err == ds.ErrNotFound {
-			return nil, errCantSaveNonExistentInstance
+			a := core.Action{
+				Type:           core.Create,
+				InstanceID:     id,
+				CollectionName: t.collection.name,
+				Previous:       nil,
+				Current:        next,
+			}
+			t.actions = append(t.actions, a)
+			// Early out and make this a create event. Assumes valid _id as specified above
+			continue
 		}
 		if err != nil {
 			return nil, err
@@ -513,7 +595,8 @@ func (t *Txn) Delete(ids ...core.InstanceID) error {
 			return err
 		}
 		if !exists {
-			return ErrInstanceNotFound
+			// Nothing to be done here
+			return nil
 		}
 		a := core.Action{
 			Type:           core.Delete,
@@ -656,11 +739,11 @@ func getSchemaTypeProperties(jt *jsonschema.Type, defs jsonschema.Definitions) (
 	if jt.Ref != "" {
 		parts := strings.Split(jt.Ref, "/")
 		if len(parts) < 1 {
-			return nil, ErrInvalidCollectionSchema
+			return make(map[string]*jsonschema.Type), nil
 		}
 		def := defs[parts[len(parts)-1]]
 		if def == nil {
-			return nil, ErrInvalidCollectionSchema
+			return make(map[string]*jsonschema.Type), nil
 		}
 		properties = def.Properties
 	}
@@ -689,6 +772,28 @@ func setNewInstanceID(t []byte) (core.InstanceID, []byte) {
 	return newID, patchedValue
 }
 
+func getModifiedTag(t []byte) (int64, error) {
+	partial := &struct {
+		Mod *int64 `json:"_mod"`
+	}{}
+	if err := json.Unmarshal(t, partial); err != nil {
+		return 0, fmt.Errorf("unmarshaling json instance: %v", err)
+	}
+	if partial.Mod == nil {
+		return 0, errMissingInstanceID
+	}
+	return *partial.Mod, nil
+}
+
+func setModifiedTag(t []byte) (newTime int64, patchedValue []byte) {
+	newTime = time.Now().UnixNano()
+	patchedValue, err := jsonpatch.MergePatch(t, []byte(fmt.Sprintf(`{"%s": %d}`, modFieldName, newTime)))
+	if err != nil {
+		log.Fatalf("while automatically patching autogenerated _mod: %v", err)
+	}
+	return
+}
+
 func (t *Txn) createEvents(actions []core.Action) (events []core.Event, node format.Node, err error) {
 	if t.discarded || t.committed {
 		return nil, nil, errAlreadyDiscardedCommitedTxn
diff --git a/db/db.go b/db/db.go
index e61a05f..291ffde 100644
--- a/db/db.go
+++ b/db/db.go
@@ -14,7 +14,7 @@ import (
 
 	"github.com/alecthomas/jsonschema"
 	"github.com/dop251/goja"
-	"github.com/ipfs/go-ipld-format"
+	format "github.com/ipfs/go-ipld-format"
 	logging "github.com/ipfs/go-log"
 	ma "github.com/multiformats/go-multiaddr"
 	ds "github.com/textileio/go-datastore"
@@ -31,6 +31,7 @@ import (
 
 const (
 	idFieldName                 = "_id"
+	modFieldName                = "_mod"
 	getBlockRetries             = 3
 	getBlockInitialTimeout      = time.Millisecond * 500
 	pullThreadBackgroundTimeout = time.Hour
@@ -43,7 +44,7 @@ var (
 	// ErrInvalidName indicates the provided name isn't valid for a Collection.
 	ErrInvalidName = errors.New("name may only contain alphanumeric characters or non-consecutive hyphens, and cannot begin or end with a hyphen")
 	// ErrInvalidCollectionSchema indicates the provided schema isn't valid for a Collection.
-	ErrInvalidCollectionSchema = errors.New("the collection schema should specify an _id string property")
+	ErrInvalidCollectionSchema = errors.New("the collection schema _id property must be a string")
 	// ErrCannotIndexIDField indicates a custom index was specified on the ID field.
 	ErrCannotIndexIDField = errors.New("cannot create custom index on " + idFieldName)
 
@@ -60,7 +61,7 @@ var (
 func init() {
 	nameRx = regexp.MustCompile(`^[A-Za-z0-9]+(?:[-][A-Za-z0-9]+)*$`)
 	// register empty map in order to gob-encode old-format events with non-encodable time.Time which cbor decodes as map[string]interface{}
-	gob.Register(map[string]interface {}{})
+	gob.Register(map[string]interface{}{})
 }
 
 // DB is the aggregate-root of events and state. External/remote events
diff --git a/db/dispatcher.go b/db/dispatcher.go
index 18f7033..bda383f 100644
--- a/db/dispatcher.go
+++ b/db/dispatcher.go
@@ -43,10 +43,14 @@ func newDispatcher(store datastore.TxnDatastore) *dispatcher {
 }
 
 // Store returns the internal event store.
-func (d *dispatcher) Store() datastore.Datastore {
+func (d *dispatcher) Store() datastore.TxnDatastore {
 	return d.store
 }
 
+func (d *dispatcher) Lock() *sync.RWMutex {
+	return &d.lock
+}
+
 // Register takes a reducer to be invoked with each dispatched event.
 func (d *dispatcher) Register(reducer Reducer) {
 	d.lock.Lock()
@@ -122,7 +126,8 @@ func getKey(event core.Event) (key datastore.Key, err error) {
 	}
 	time := strconv.FormatInt(unix, 10)
 	key = dsDispatcherPrefix.ChildString(time).
-		ChildString(event.InstanceID().String()).
-		ChildString(event.Collection())
+		ChildString(event.Collection()).
+		Instance(event.InstanceID().String())
+
 	return key, nil
 }
diff --git a/go.mod b/go.mod
index 799fdf1..fc0f38b 100644
--- a/go.mod
+++ b/go.mod
@@ -7,14 +7,12 @@ require (
 	// agl/ed25519 only used in tests for backward compatibility, *do not* use in production code.
 	github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412
 	github.com/alecthomas/jsonschema v0.0.0-20191017121752-4bb6e3fae4f2
-	github.com/c-bata/go-prompt v0.2.3
 	github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
 	github.com/dgraph-io/badger v1.6.1
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/dlclark/regexp2 v1.2.0 // indirect
 	github.com/dop251/goja v0.0.0-20200721192441-a695b0cdd498
 	github.com/evanphx/json-patch v4.5.0+incompatible
-	github.com/fatih/color v1.7.0
 	github.com/fsnotify/fsnotify v1.4.7
 	github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
 	github.com/gogo/googleapis v1.3.1 // indirect
@@ -34,6 +32,7 @@ require (
 	github.com/ipfs/go-ipfs-blockstore v1.0.1
 	github.com/ipfs/go-ipfs-exchange-offline v0.0.1
 	github.com/ipfs/go-ipfs-files v0.0.4 // indirect
+	github.com/ipfs/go-ipfs-util v0.0.2
 	github.com/ipfs/go-ipld-cbor v0.0.4
 	github.com/ipfs/go-ipld-format v0.2.0
 	github.com/ipfs/go-log v1.0.4
@@ -47,10 +46,6 @@ require (
 	github.com/libp2p/go-libp2p-peerstore v0.2.6
 	github.com/libp2p/go-libp2p-pubsub v0.2.4
 	github.com/libp2p/go-libp2p-swarm v0.2.8
-	github.com/mattn/go-isatty v0.0.11 // indirect
-	github.com/mattn/go-runewidth v0.0.8 // indirect
-	github.com/mattn/go-tty v0.0.3 // indirect
-	github.com/mitchellh/go-homedir v1.1.0
 	github.com/multiformats/go-multiaddr v0.2.2
 	github.com/multiformats/go-multibase v0.0.3
 	github.com/multiformats/go-multihash v0.0.14
@@ -59,7 +54,6 @@ require (
 	github.com/namsral/flag v1.7.4-pre
 	github.com/oklog/ulid/v2 v2.0.2
 	github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2
-	github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect
 	github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1 // indirect
 	github.com/rs/cors v1.7.0 // indirect
 	github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 // indirect
@@ -72,6 +66,7 @@ require (
 	github.com/xeipuuv/gojsonschema v1.2.0
 	go.uber.org/zap v1.15.0
 	golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5
+	golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5
 	golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
 	golang.org/x/text v0.3.3 // indirect
 	google.golang.org/genproto v0.0.0-20200428115010-c45acf45369a // indirect
diff --git a/go.sum b/go.sum
index 1cc02ee..3d0810f 100644
--- a/go.sum
+++ b/go.sum
@@ -51,8 +51,6 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku
 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
 github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
 github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
-github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s=
-github.com/c-bata/go-prompt v0.2.3/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34=
 github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
 github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -108,8 +106,6 @@ github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5I
 github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5 h1:BBso6MBKW8ncyZLv37o+KNyy0HrrHgfnOaGQC2qvN+A=
 github.com/facebookgo/atomicfile v0.0.0-20151019160806-2de1f203e7d5/go.mod h1:JpoxHjuQauoxiFMl1ie8Xc/7TfLuMZ5eOCONd1sUBHg=
-github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fd/go-nat v1.0.0/go.mod h1:BTBu/CKvMmOMUPkKVef1pngt2WFH/lg7E6yQnulfp6E=
 github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
 github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
@@ -730,30 +726,16 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
 github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
-github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
 github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
-github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
-github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
-github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
-github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
-github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
-github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
 github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
 github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.12 h1:WMhc1ik4LNkTg8U9l3hI1LvxKmIL+f1+WV/SZtCbDDA=
 github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/miekg/dns v1.1.28 h1:gQhy5bsJa8zTlVI8lywCTZp1lguor+xevFoYlzeCTQY=
 github.com/miekg/dns v1.1.28/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
-github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
 github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g=
 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ=
@@ -770,9 +752,7 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/mr-tron/base58 v1.1.0/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
 github.com/mr-tron/base58 v1.1.1/go.mod h1:xcD2VGqlgYjBdcBLw+TuYLr8afG+Hj8g2eTVqeSzSU8=
-github.com/mr-tron/base58 v1.1.2 h1:ZEw4I2EgPKDJ2iEw0cNmLB3ROrEmkOtXIkaG7wZg+78=
 github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
-github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
 github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
 github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
@@ -784,18 +764,14 @@ github.com/multiformats/go-multiaddr v0.0.1/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lg
 github.com/multiformats/go-multiaddr v0.0.2/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
 github.com/multiformats/go-multiaddr v0.0.4/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
 github.com/multiformats/go-multiaddr v0.1.0/go.mod h1:xKVEak1K9cS1VdmPZW3LSIb6lgmoS58qz/pzqmAxV44=
-github.com/multiformats/go-multiaddr v0.1.1 h1:rVAztJYMhCQ7vEFr8FvxW3mS+HF2eY/oPbOMeS0ZDnE=
 github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo=
 github.com/multiformats/go-multiaddr v0.1.2/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
-github.com/multiformats/go-multiaddr v0.2.0 h1:lR52sFwcTCuQb6bTfnXF6zA2XfyYvyd+5a9qECv/J90=
 github.com/multiformats/go-multiaddr v0.2.0/go.mod h1:0nO36NvPpyV4QzvTLi/lafl2y95ncPj0vFwVF6k6wJ4=
-github.com/multiformats/go-multiaddr v0.2.1 h1:SgG/cw5vqyB5QQe5FPe2TqggU9WtrA9X4nZw7LlVqOI=
 github.com/multiformats/go-multiaddr v0.2.1/go.mod h1:s/Apk6IyxfvMjDafnhJgJ3/46z7tZ04iMk5wP4QMGGE=
 github.com/multiformats/go-multiaddr v0.2.2 h1:XZLDTszBIJe6m0zF6ITBrEcZR73OPUhCBBS9rYAuUzI=
 github.com/multiformats/go-multiaddr v0.2.2/go.mod h1:NtfXiOtHvghW9KojvtySjH5y0u0xW5UouOmQQrn6a3Y=
 github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
 github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q=
-github.com/multiformats/go-multiaddr-dns v0.1.0 h1:gsPeMvo91XvcsNlQXgJgfjYjbsVV99bvveguUvDBpyQ=
 github.com/multiformats/go-multiaddr-dns v0.1.0/go.mod h1:01k2RAqtoXIuPa3DCavAE9/6jc6nM0H3EgZyfUhN2oY=
 github.com/multiformats/go-multiaddr-dns v0.2.0 h1:YWJoIDwLePniH7OU5hBnDZV6SWuvJqJ0YtN6pLeH9zA=
 github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0=
@@ -803,43 +779,32 @@ github.com/multiformats/go-multiaddr-fmt v0.0.1/go.mod h1:aBYjqL4T/7j4Qx+R73XSv/
 github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E=
 github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo=
 github.com/multiformats/go-multiaddr-net v0.0.1/go.mod h1:nw6HSxNmCIQH27XPGBuX+d1tnvM7ihcFwHMSstNAVUU=
-github.com/multiformats/go-multiaddr-net v0.1.0 h1:ZepO8Ezwovd+7b5XPPDhQhayk1yt0AJpzQBpq9fejx4=
 github.com/multiformats/go-multiaddr-net v0.1.0/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
-github.com/multiformats/go-multiaddr-net v0.1.1 h1:jFFKUuXTXv+3ARyHZi3XUqQO+YWMKgBdhEvuGRfnL6s=
 github.com/multiformats/go-multiaddr-net v0.1.1/go.mod h1:5JNbcfBOP4dnhoZOv10JJVkJO0pCCEf8mTnipAo2UZQ=
 github.com/multiformats/go-multiaddr-net v0.1.2/go.mod h1:QsWt3XK/3hwvNxZJp92iMQKME1qHfpYmyIjFVsSOY6Y=
 github.com/multiformats/go-multiaddr-net v0.1.3/go.mod h1:ilNnaM9HbmVFqsb/qcNysjCu4PVONlrBZpHIrw/qQuA=
-github.com/multiformats/go-multiaddr-net v0.1.4 h1:g6gwydsfADqFvrHoMkS0n9Ok9CG6F7ytOH/bJDkhIOY=
 github.com/multiformats/go-multiaddr-net v0.1.4/go.mod h1:ilNnaM9HbmVFqsb/qcNysjCu4PVONlrBZpHIrw/qQuA=
 github.com/multiformats/go-multiaddr-net v0.1.5 h1:QoRKvu0xHN1FCFJcMQLbG/yQE2z441L5urvG3+qyz7g=
 github.com/multiformats/go-multiaddr-net v0.1.5/go.mod h1:ilNnaM9HbmVFqsb/qcNysjCu4PVONlrBZpHIrw/qQuA=
-github.com/multiformats/go-multibase v0.0.1 h1:PN9/v21eLywrFWdFNsFKaU04kLJzuYzmrJR+ubhT9qA=
 github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs=
 github.com/multiformats/go-multibase v0.0.3 h1:l/B6bJDQjvQ5G52jw4QGSYeOTZoAwIO77RblWplfIqk=
 github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc=
 github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U=
 github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po=
 github.com/multiformats/go-multihash v0.0.7/go.mod h1:XuKXPp8VHcTygube3OWZC+aZrA+H1IhmjoCDtJc7PXM=
-github.com/multiformats/go-multihash v0.0.8 h1:wrYcW5yxSi3dU07n5jnuS5PrNwyHy0zRHGVoUugWvXg=
 github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
 github.com/multiformats/go-multihash v0.0.9/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
-github.com/multiformats/go-multihash v0.0.10 h1:lMoNbh2Ssd9PUF74Nz008KGzGPlfeV6wH3rit5IIGCM=
 github.com/multiformats/go-multihash v0.0.10/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew=
-github.com/multiformats/go-multihash v0.0.13 h1:06x+mk/zj1FoMsgNejLpy6QTvJqlSt/BhLEy87zidlc=
 github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
 github.com/multiformats/go-multihash v0.0.14 h1:QoBceQYQQtNUuf6s7wHxnE2c8bhbMqhfGzNI032se/I=
 github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc=
 github.com/multiformats/go-multistream v0.0.1/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg=
-github.com/multiformats/go-multistream v0.1.0 h1:UpO6jrsjqs46mqAK3n6wKRYFhugss9ArzbyUzU+4wkQ=
 github.com/multiformats/go-multistream v0.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg=
-github.com/multiformats/go-multistream v0.1.1 h1:JlAdpIFhBhGRLxe9W6Om0w++Gd6KMWoFPZL/dEnm9nI=
 github.com/multiformats/go-multistream v0.1.1/go.mod h1:KmHZ40hzVxiaiwlj3MEbYgK9JFk2/9UktWZAF54Du38=
 github.com/multiformats/go-multistream v0.1.2 h1:knyamLYMPFPngQjGQ0lhnlys3jtVR/3xV6TREUJr+fE=
 github.com/multiformats/go-multistream v0.1.2/go.mod h1:5GZPQZbkWOLOn3J2y4Y99vVW7vOfsAflxARk3x14o6k=
-github.com/multiformats/go-varint v0.0.1 h1:TR/0rdQtnNxuN2IhiB639xC3tWM4IUi7DkTBVTdGW/M=
 github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
 github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
-github.com/multiformats/go-varint v0.0.5 h1:XVZwSo04Cs3j/jS0uAEPpT3JY6DzMcVLLoWOSnCxOjg=
 github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
 github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
 github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
@@ -855,22 +820,18 @@ github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
 github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
 github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
 github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg=
 github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg=
 github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
-github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
 github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
@@ -879,17 +840,12 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwp
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc=
 github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 h1:A7GG7zcGjl3jqAqGPmcNjd/D9hzL95SuoOQAaFNdLU0=
-github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992 h1:bzMe+2coZJYHnhGgVlcQKuRy4FSny4ds8dLQjw5P1XE=
 github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
-github.com/polydawn/refmt v0.0.0-20190408063855-01bf1e26dd14 h1:2m16U/rLwVaRdz7ANkHtHTodP3zTP3N451MADg64x5k=
 github.com/polydawn/refmt v0.0.0-20190408063855-01bf1e26dd14/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
 github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1 h1:CskT+S6Ay54OwxBGB0R3Rsx4Muto6UnEYTyKJbyRIAI=
 github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o=
@@ -925,16 +881,12 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l
 github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4=
 github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw=
-github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
 github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
 github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8=
 github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
-github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa h1:E+gaaifzi2xF65PbDmuKI3PhLWY6G5opMLniFq8vmXA=
 github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU=
-github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
 github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
 github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945 h1:N8Bg45zpk/UcpNGnfJt2y/3lRWASHNTUET8owPYCgYI=
 github.com/smartystreets/goconvey v0.0.0-20190710185942-9d28bd7c0945/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@@ -949,24 +901,17 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
-github.com/src-d/envconfig v1.0.0 h1:/AJi6DtjFhZKNx3OB2qMsq7y4yT5//AeSZIe7rk+PX8=
 github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709 h1:Ko2LQMrRU+Oy/+EDBwX7eZ2jp3C47eDBB8EIhKTun+I=
 github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
@@ -990,7 +935,6 @@ github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljT
 github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
 github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
 github.com/wangjia184/sortedset v0.0.0-20160527075905-f5d03557ba30/go.mod h1:YkocrP2K2tcw938x9gCOmT5G5eCD6jsTz0SZuyAqwIE=
-github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436 h1:qOpVTI+BrstcjTZLm2Yz/3sOnqkzj3FQoh0g+E5s3Gc=
 github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
 github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830 h1:8kxMKmKzXXL4Ru1nyhvdms/JjWt+3YLpvRb/bAjO/y0=
 github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
@@ -1002,28 +946,22 @@ github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP
 github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8=
 github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k=
 github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc=
-github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc h1:9lDbC6Rz4bwmou+oE6Dt4Cb2BGMur5eR/GYptkKUVHo=
 github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM=
 github.com/whyrusleeping/go-logging v0.0.1/go.mod h1:lDPYj54zutzG1XYfHAhcc7oNXEburHQBn+Iqd4yS4vE=
-github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f h1:M/lL30eFZTKnomXY6huvM6G0+gVquFNf6mxghaWlFUg=
 github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f/go.mod h1:cZNvX9cFybI01GriPRMXDtczuvUhgbcYr9iCGaNlRv8=
 github.com/whyrusleeping/go-smux-multiplex v3.0.16+incompatible/go.mod h1:34LEDbeKFZInPUrAG+bjuJmUXONGdEFW7XL0SpTY1y4=
 github.com/whyrusleeping/go-smux-multistream v2.0.2+incompatible/go.mod h1:dRWHHvc4HDQSHh9gbKEBbUZ+f2Q8iZTPG3UOGYODxSQ=
 github.com/whyrusleeping/go-smux-yamux v2.0.8+incompatible/go.mod h1:6qHUzBXUbB9MXmw3AUdB52L8sEb/hScCqOdW2kj/wuI=
 github.com/whyrusleeping/go-smux-yamux v2.0.9+incompatible/go.mod h1:6qHUzBXUbB9MXmw3AUdB52L8sEb/hScCqOdW2kj/wuI=
-github.com/whyrusleeping/mafmt v1.2.8 h1:TCghSl5kkwEE0j+sU/gudyhVMRlpBin8fMBBHg59EbA=
 github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA=
 github.com/whyrusleeping/mdns v0.0.0-20180901202407-ef14215e6b30/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
-github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9 h1:Y1/FEOpaCpD21WxrmfeIYCFPuVPRCY2XZTWzTNHGw30=
 github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4=
 github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7 h1:E9S12nwJwEOXe2d6gT6qxdvqMnNq+VnSsKPgm2ZZNds=
 github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI=
 github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee h1:lYbXeSvJi5zk5GLKVuid9TVjS9a0OmLIDKTfoZBL6Ow=
 github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:m2aV4LZI4Aez7dP5PMyVKEHhUyEJ/RjmPEDOpDvudHg=
 github.com/whyrusleeping/yamux v1.1.5/go.mod h1:E8LnQQ8HKx5KD29HZFUwM1PxCOdPRzGwur1mcYhXcD8=
-github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
-github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
 github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -1034,29 +972,22 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50=
 go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA=
-go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
 go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
 go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
 go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
-go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
 go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
-go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
 go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-go.uber.org/zap v1.14.1 h1:nYDKopTbvAPq/NrUVZwT15y2lpROBiLLyoRTbXOYWOo=
 go.uber.org/zap v1.14.1/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
 go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
 go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
@@ -1077,16 +1008,14 @@ golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392 h1:ACG4HJsFiNMf47Y4PeRoebLNy/2lXT9EtprMuTFWt1M=
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
 golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5 h1:Q7tZBpemrlsc2I7IyODzhtallWRSm4Q0d09pL6XbQtU=
 golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 h1:FR+oGxGfbQu1d+jglI3rCkjAjUnhRSZcUxr+DqlDLNo=
 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1117,7 +1046,6 @@ golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200421231249-e086a090c8fd h1:QPwSajcTUrFriMF1nJ3XzgoqakqQEsnZf9LdXdi2nkI=
@@ -1130,12 +1058,8 @@ golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5Tlb
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI=
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
-golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
 golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1159,15 +1083,11 @@ golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM=
 golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
@@ -1175,7 +1095,6 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -1197,13 +1116,10 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
 golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa h1:5E4dL8+NgFOgjwbTKz+OOEGGhP+ectTmF842l6KjupQ=
 golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1221,9 +1137,7 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoA
 google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
 google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb h1:i1Ppqkc3WQXikh8bXiwHqAN5Rv3/qDCcRk0/Otx73BY=
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
 google.golang.org/genproto v0.0.0-20200428115010-c45acf45369a h1:ykRcNp3dotYGpAEIYeWCGaefklVjVy/rnSvM3zNh6j8=
@@ -1233,13 +1147,10 @@ google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmE
 google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
 google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0=
 google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
 google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
 google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
@@ -1251,21 +1162,17 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi
 google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
 gopkg.in/src-d/go-cli.v0 v0.0.0-20181105080154-d492247bbc0d/go.mod h1:z+K8VcOYVYcSwSjGebuDL6176A1XskgbtNl64NSg+n8=
-gopkg.in/src-d/go-log.v1 v1.0.1 h1:heWvX7J6qbGWbeFS/aRmiy1eYaT+QMV6wNvHDyMjQV4=
 gopkg.in/src-d/go-log.v1 v1.0.1/go.mod h1:GN34hKP0g305ysm2/hctJ0Y8nWP3zxXXJ8GFabTyABE=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/net/client.go b/net/client.go
index 2498073..bd57b30 100644
--- a/net/client.go
+++ b/net/client.go
@@ -2,6 +2,7 @@ package net
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	nnet "net"
 	"sync"
@@ -121,66 +122,21 @@ func (s *server) pushLog(ctx context.Context, id thread.ID, lg thread.LogInfo, p
 	return err
 }
 
-// records maintains an ordered list of records from multiple sources.
-type records struct {
-	sync.RWMutex
-	m map[peer.ID]map[cid.Cid]core.Record
-	s map[peer.ID][]core.Record
-}
-
-// newRecords creates an instance of records.
-func newRecords() *records {
-	return &records{
-		m: make(map[peer.ID]map[cid.Cid]core.Record),
-		s: make(map[peer.ID][]core.Record),
-	}
-}
-
-// List all records.
-func (r *records) List() map[peer.ID][]core.Record {
-	r.RLock()
-	defer r.RUnlock()
-	return r.s
-}
-
-// Store a record.
-func (r *records) Store(p peer.ID, key cid.Cid, value core.Record) {
-	r.Lock()
-	defer r.Unlock()
-	if _, ok := r.m[p]; !ok {
-		r.m[p] = make(map[cid.Cid]core.Record)
-		r.s[p] = make([]core.Record, 0)
-	}
-	if _, ok := r.m[p][key]; ok {
-		return
-	}
-	r.m[p][key] = value
-
-	// Sanity check
-	if len(r.s[p]) > 0 && r.s[p][len(r.s[p])-1].Cid() != value.PrevID() {
-		panic("there is a gap in records list")
-	}
-
-	r.s[p] = append(r.s[p], value)
-}
-
 // getRecords from log addresses.
 func (s *server) getRecords(
 	ctx context.Context,
 	tid thread.ID,
-	lid peer.ID,
 	offsets map[peer.ID]cid.Cid,
 	limit int,
 ) (map[peer.ID][]core.Record, error) {
 	sk, err := s.net.store.ServiceKey(tid)
 	if err != nil {
 		return nil, err
-	}
-	if sk == nil {
-		return nil, fmt.Errorf("a service-key is required to request records")
+	} else if sk == nil {
+		return nil, errors.New("a service-key is required to request records")
 	}
 
-	pblgs := make([]*pb.GetRecordsRequest_Body_LogEntry, 0, len(offsets))
+	var pblgs = make([]*pb.GetRecordsRequest_Body_LogEntry, 0, len(offsets))
 	for lid, offset := range offsets {
 		pblgs = append(pblgs, &pb.GetRecordsRequest_Body_LogEntry{
 			LogID:  &pb.ProtoPeerID{ID: lid},
@@ -194,10 +150,12 @@ func (s *server) getRecords(
 		ServiceKey: &pb.ProtoKey{Key: sk},
 		Logs:       pblgs,
 	}
+
 	sig, key, err := s.signRequestBody(body)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("signing GetRecords request: %w", err)
 	}
+
 	req := &pb.GetRecordsRequest{
 		Header: &pb.Header{
 			PubKey:    &pb.ProtoPubKey{PubKey: key},
@@ -206,23 +164,30 @@ func (s *server) getRecords(
 		Body: body,
 	}
 
-	logAddrs, err := s.net.store.Addrs(tid, lid)
-	if err != nil {
-		return nil, err
+	// set of unique log addresses
+	var logAddrs = make(map[ma.Multiaddr]struct{}, len(offsets))
+	for lid := range offsets {
+		las, err := s.net.store.Addrs(tid, lid)
+		if err != nil {
+			return nil, err
+		}
+		for _, addr := range las {
+			logAddrs[addr] = struct{}{}
+		}
 	}
 
 	var (
-		recs = newRecords()
-		wg   sync.WaitGroup
+		rc = newRecordCollector()
+		wg sync.WaitGroup
 	)
 
 	// Pull from each address
-	for _, addr := range logAddrs {
+	for addr := range logAddrs {
 		wg.Add(1)
 
 		go withErrLog(addr, func(addr ma.Multiaddr) error {
 			defer wg.Done()
-			pid, ok, err := s.callablePeer(addr)
+			pid, ok, err := s.net.callablePeer(addr)
 			if err != nil {
 				return err
 			} else if !ok {
@@ -279,7 +244,8 @@ func (s *server) getRecords(
 					if err = rec.Verify(pk); err != nil {
 						return err
 					}
-					recs.Store(logID, rec.Cid(), rec)
+
+					rc.Store(logID, rec)
 				}
 			}
 			return nil
@@ -287,7 +253,7 @@ func (s *server) getRecords(
 	}
 	wg.Wait()
 
-	return recs.List(), nil
+	return rc.List()
 }
 
 // pushRecord to log addresses and thread topic.
@@ -326,7 +292,7 @@ func (s *server) pushRecord(ctx context.Context, id thread.ID, lid peer.ID, rec
 	// Push to each address
 	for _, addr := range addrs {
 		go withErrLog(addr, func(addr ma.Multiaddr) error {
-			pid, ok, err := s.callablePeer(addr)
+			pid, ok, err := s.net.callablePeer(addr)
 			if err != nil {
 				return err
 			} else if !ok {
@@ -385,25 +351,6 @@ func (s *server) pushRecord(ctx context.Context, id thread.ID, lid peer.ID, rec
 	return nil
 }
 
-// callablePeer attempts to obtain external peer ID from the multiaddress.
-func (s *server) callablePeer(addr ma.Multiaddr) (peer.ID, bool, error) {
-	p, err := addr.ValueForProtocol(ma.P_P2P)
-	if err != nil {
-		return "", false, err
-	}
-
-	pid, err := peer.Decode(p)
-	if err != nil {
-		return "", false, err
-	}
-
-	if pid.String() == s.net.host.ID().String() {
-		return pid, false, nil
-	}
-
-	return pid, true, nil
-}
-
 // dial attempts to open a gRPC connection over libp2p to a peer.
 func (s *server) dial(peerID peer.ID) (pb.ServiceClient, error) {
 	s.Lock()
diff --git a/net/net.go b/net/net.go
index 5745d88..b3640d6 100644
--- a/net/net.go
+++ b/net/net.go
@@ -39,7 +39,10 @@ var (
 	// MaxPullLimit is the maximum page size for pulling records.
 	MaxPullLimit = 10000
 
-	// InitialPullInterval is the interval between automatic log pulls.
+	// PullStartAfter is the pause before thread pulling process starts.
+	PullStartAfter = time.Second
+
+	// InitialPullInterval is the interval for the first iteration of log pulls.
 	InitialPullInterval = time.Second
 
 	// PullInterval is the interval between automatic log pulls.
@@ -393,70 +396,30 @@ func (n *net) PullThread(ctx context.Context, id thread.ID, opts ...core.ThreadO
 }
 
 // pullThread for the new records. This method is thread-safe.
-func (n *net) pullThread(ctx context.Context, id thread.ID) error {
-	tps := n.semaphores.Get(semaThreadPull(id))
+func (n *net) pullThread(ctx context.Context, tid thread.ID) error {
+	tps := n.semaphores.Get(semaThreadPull(tid))
 	if !tps.TryAcquire() {
-		log.Debugf("skip pulling thread %s: concurrent pull in progress", id)
+		log.Debugf("skip pulling thread %s: concurrent pull in progress", tid)
 		return nil
 	}
 
-	info, err := n.store.GetThread(id)
+	offsets, err := n.threadOffsets(tid)
 	if err != nil {
 		tps.Release()
 		return err
 	}
 
-	// Gather offsets for each log
-	offsets := make(map[peer.ID]cid.Cid)
-	for _, lg := range info.Logs {
-		var has bool
-		if lg.Head.Defined() {
-			has, err = n.bstore.Has(lg.Head)
-			if err != nil {
-				tps.Release()
-				return err
-			}
-		}
-		if has {
-			offsets[lg.ID] = lg.Head
-		} else {
-			offsets[lg.ID] = cid.Undef
-		}
-	}
-
-	var (
-		fetchedRcs []map[peer.ID][]core.Record
-		lock       sync.Mutex
-		wg         sync.WaitGroup
-	)
-
-	for _, lg := range info.Logs {
-		wg.Add(1)
-		go func(lg thread.LogInfo) {
-			defer wg.Done()
-			// Pull from addresses
-			recs, err := n.server.getRecords(ctx, id, lg.ID, offsets, MaxPullLimit)
-			if err != nil {
-				log.Error(err)
-				return
-			}
-
-			lock.Lock()
-			fetchedRcs = append(fetchedRcs, recs)
-			lock.Unlock()
-		}(lg)
-	}
-	wg.Wait()
+	// Pull from addresses
+	recs, err := n.server.getRecords(ctx, tid, offsets, MaxPullLimit)
 	tps.Release()
+	if err != nil {
+		return err
+	}
 
-	// maybe we should preliminary deduplicate records?
-	for _, recs := range fetchedRcs {
-		for lid, rs := range recs {
-			for _, r := range rs {
-				if err = n.putRecord(ctx, id, lid, r); err != nil {
-					log.Error(err)
-					return err
-				}
+	for lid, rs := range recs {
+		for _, r := range rs {
+			if err = n.putRecord(ctx, tid, lid, r); err != nil {
+				return err
 			}
 		}
 	}
@@ -599,22 +562,18 @@ func (n *net) AddReplicator(ctx context.Context, id thread.ID, paddr ma.Multiadd
 		wg.Add(1)
 		go func(addr ma.Multiaddr) {
 			defer wg.Done()
-			p, err := addr.ValueForProtocol(ma.P_P2P)
+			pid, ok, err := n.callablePeer(addr)
 			if err != nil {
 				log.Error(err)
 				return
-			}
-			pid, err := peer.Decode(p)
-			if err != nil {
-				log.Error(err)
-				return
-			}
-			if pid.String() == n.host.ID().String() {
+			} else if !ok {
+				// skip calling itself
 				return
 			}
+
 			for _, lg := range managedLogs {
 				if err = n.server.pushLog(ctx, info.ID, lg, pid, nil, nil); err != nil {
-					log.Errorf("error pushing log %s to %s", lg.ID, p)
+					log.Errorf("error pushing log %s to %s: %v", lg.ID, pid, err)
 				}
 			}
 		}(addr)
@@ -624,6 +583,25 @@ func (n *net) AddReplicator(ctx context.Context, id thread.ID, paddr ma.Multiadd
 	return pid, nil
 }
 
+// callablePeer attempts to obtain external peer ID from the multiaddress.
+func (n *net) callablePeer(addr ma.Multiaddr) (peer.ID, bool, error) {
+	p, err := addr.ValueForProtocol(ma.P_P2P)
+	if err != nil {
+		return "", false, err
+	}
+
+	pid, err := peer.Decode(p)
+	if err != nil {
+		return "", false, err
+	}
+
+	if pid.String() == n.host.ID().String() {
+		return pid, false, nil
+	}
+
+	return pid, true, nil
+}
+
 func getDialable(addr ma.Multiaddr) (ma.Multiaddr, error) {
 	parts := strings.Split(addr.String(), "/"+ma.ProtocolWithCode(ma.P_P2P).Name)
 	return ma.NewMultiaddr(parts[0])
@@ -1138,38 +1116,61 @@ func (n *net) deleteRecord(ctx context.Context, rid cid.Cid, sk *sym.Key) (prev
 
 // startPulling periodically pulls on all threads.
 func (n *net) startPulling() {
-	pull := func() {
-		ts, err := n.store.Threads()
-		if err != nil {
-			log.Errorf("error listing threads: %s", err)
-			return
-		}
-		for _, id := range ts {
-			go func(id thread.ID) {
-				if err := n.pullThread(n.ctx, id); err != nil {
-					log.Errorf("error pulling thread %s: %s", id, err)
-				}
-			}(id)
-		}
-	}
-	timer := time.NewTimer(InitialPullInterval)
 	select {
-	case <-timer.C:
-		pull()
+	case <-time.After(PullStartAfter):
 	case <-n.ctx.Done():
 		return
 	}
 
-	tick := time.NewTicker(PullInterval)
-	defer tick.Stop()
+	// set pull cycle interval into initial value,
+	// it will be redefined on the next iteration
+	var interval = InitialPullInterval
 
+PullCycle:
 	for {
-		select {
-		case <-tick.C:
-			pull()
-		case <-n.ctx.Done():
+		ts, err := n.store.Threads()
+		if err != nil {
+			log.Errorf("error listing threads: %s", err)
 			return
 		}
+
+		if len(ts) == 0 {
+			// if there are no threads served, just wait and retry
+			select {
+			case <-time.After(interval):
+				interval = PullInterval
+				continue PullCycle
+			case <-n.ctx.Done():
+				return
+			}
+		}
+
+		var (
+			period = interval / time.Duration(len(ts))
+			ticker = time.NewTicker(period)
+			idx    = 0
+		)
+
+		for {
+			select {
+			case <-ticker.C:
+				go func(tid thread.ID) {
+					if err := n.pullThread(n.ctx, tid); err != nil {
+						log.Errorf("error pulling thread %s: %s", tid, err)
+					}
+				}(ts[idx])
+				idx++
+				if idx >= len(ts) {
+					ticker.Stop()
+					interval = PullInterval
+					continue PullCycle
+				}
+
+			case <-n.ctx.Done():
+				ticker.Stop()
+				return
+			}
+		}
 	}
 }
 
@@ -1330,25 +1331,57 @@ func (n *net) updateRecordsFromLog(tid thread.ID, lid peer.ID) {
 		return
 	}
 
-	// Get log records for this new log
-	recs, err := n.server.getRecords(
-		n.ctx,
-		tid,
-		lid,
-		map[peer.ID]cid.Cid{lid: cid.Undef},
-		MaxPullLimit)
+	// TODO after protocol change request only new log
+	offsets, err := n.threadOffsets(tid)
+	if err != nil {
+		tps.Release()
+		log.Errorf("getting offsets for thread %s failed: %v", tid, err)
+		return
+	}
+
+	// request records for the new log
+	offsets[lid] = cid.Undef
+
+	recs, err := n.server.getRecords(n.ctx, tid, offsets, MaxPullLimit)
 	tps.Release()
 	if err != nil {
-		log.Error(err)
+		log.Errorf("getting records from new log %s (thread %s) failed: %v", lid, tid, err)
 		return
 	}
 
 	for lid, rs := range recs {
 		for _, r := range rs {
 			if err = n.putRecord(n.ctx, tid, lid, r); err != nil {
-				log.Error(err)
+				log.Errorf("putting records from new log %s (thread %s) failed: %v", lid, tid, err)
 				return
 			}
 		}
 	}
 }
+
+// get offsets for all known thread's logs
+func (n *net) threadOffsets(tid thread.ID) (map[peer.ID]cid.Cid, error) {
+	info, err := n.store.GetThread(tid)
+	if err != nil {
+		return nil, err
+	}
+
+	var offsets = make(map[peer.ID]cid.Cid, len(info.Logs))
+	for _, lg := range info.Logs {
+		var has bool
+		if lg.Head.Defined() {
+			has, err = n.bstore.Has(lg.Head)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		if has {
+			offsets[lg.ID] = lg.Head
+		} else {
+			offsets[lg.ID] = cid.Undef
+		}
+	}
+
+	return offsets, nil
+}
diff --git a/net/record.go b/net/record.go
new file mode 100644
index 0000000..1a67acd
--- /dev/null
+++ b/net/record.go
@@ -0,0 +1,130 @@
+package net
+
+import (
+	"fmt"
+	"sync"
+
+	"github.com/ipfs/go-cid"
+	"github.com/libp2p/go-libp2p-core/peer"
+	core "github.com/textileio/go-threads/core/net"
+)
+
+type linkedRecord interface {
+	Cid() cid.Cid
+	PrevID() cid.Cid
+}
+
+// Collector maintains an ordered list of records from multiple sources (thread-safe)
+type recordCollector struct {
+	rs   map[peer.ID]*recordSequence
+	lock sync.Mutex
+}
+
+func newRecordCollector() *recordCollector {
+	return &recordCollector{rs: make(map[peer.ID]*recordSequence)}
+}
+
+// Store the record of the log.
+func (r *recordCollector) Store(lid peer.ID, rec core.Record) {
+	r.lock.Lock()
+	defer r.lock.Unlock()
+
+	seq, found := r.rs[lid]
+	if !found {
+		seq = newRecordSequence()
+		r.rs[lid] = seq
+	}
+
+	seq.Store(rec)
+}
+
+// List all previously stored records in a proper order if the latter exists.
+func (r *recordCollector) List() (map[peer.ID][]core.Record, error) {
+	r.lock.Lock()
+	defer r.lock.Unlock()
+
+	logSeqs := make(map[peer.ID][]core.Record, len(r.rs))
+	for id, seq := range r.rs {
+		ordered, ok := seq.List()
+		if !ok {
+			return nil, fmt.Errorf("disjoint record sequence in log %s", id)
+		}
+		casted := make([]core.Record, len(ordered))
+		for i := 0; i < len(ordered); i++ {
+			casted[i] = ordered[i].(core.Record)
+		}
+		logSeqs[id] = casted
+	}
+
+	return logSeqs, nil
+}
+
+// Not a thread-safe structure
+type recordSequence struct {
+	fragments [][]linkedRecord
+	set       map[cid.Cid]struct{}
+}
+
+func newRecordSequence() *recordSequence {
+	return &recordSequence{set: make(map[cid.Cid]struct{})}
+}
+
+func (s *recordSequence) Store(rec linkedRecord) {
+	// verify if record is already contained in some fragment
+	if _, found := s.set[rec.Cid()]; found {
+		return
+	}
+	s.set[rec.Cid()] = struct{}{}
+
+	// now try to find a sequence to be attached to
+	for i, fragment := range s.fragments {
+		if fragment[len(fragment)-1].Cid() == rec.PrevID() {
+			s.fragments[i] = append(fragment, rec)
+			return
+		} else if fragment[0].PrevID() == rec.Cid() {
+			s.fragments[i] = append([]linkedRecord{rec}, fragment...)
+			return
+		}
+	}
+
+	// start a new fragment
+	s.fragments = append(s.fragments, []linkedRecord{rec})
+}
+
+// return reconstructed sequence and success flag
+func (s *recordSequence) List() ([]linkedRecord, bool) {
+LOOP:
+	// avoid recursion as sequences could be pretty large
+	for {
+		if len(s.fragments) == 1 {
+			return s.fragments[0], true
+		}
+
+		// take a fragment ...
+		fragment := s.fragments[0]
+		fHead, fTail := fragment[len(fragment)-1], fragment[0]
+
+		// ... and try to compose it with another one
+		for i, candidate := range s.fragments[1:] {
+			cHead, cTail := candidate[len(candidate)-1], candidate[0]
+			// index shifted by slicing
+			i += 1
+
+			if fHead.Cid() == cTail.PrevID() {
+				// composition: (tail) <- fragment <- candidate <- (head)
+				s.fragments[0] = append(fragment, candidate...)
+				s.fragments = append(s.fragments[:i], s.fragments[i+1:]...)
+				continue LOOP
+
+			} else if fTail.PrevID() == cHead.Cid() {
+				// composition: (tail) <- candidate <- fragment <- (head)
+				s.fragments[i] = append(candidate, fragment...)
+				s.fragments = s.fragments[1:]
+				continue LOOP
+			}
+		}
+
+		// no composition found, hence there are at least two disjoint fragments
+		return nil, false
+	}
+}


Test patch:
diff --git a/db/collection_test.go b/db/collection_test.go
index ecd5418..89076e8 100644
--- a/db/collection_test.go
+++ b/db/collection_test.go
@@ -7,6 +7,7 @@ import (
 	"os"
 	"reflect"
 	"testing"
+	"time"
 
 	logging "github.com/ipfs/go-log"
 	core "github.com/textileio/go-threads/core/db"
@@ -20,12 +21,14 @@ const (
 
 type Person struct {
 	ID   core.InstanceID `json:"_id"`
+	Mod  int64           `json:"_mod"`
 	Name string
 	Age  int
 }
 
 type Person2 struct {
 	ID       core.InstanceID `json:"_id"`
+	Mod      int64           `json:"_mod"`
 	Name     string
 	Age      int
 	Toys     Toys
@@ -750,7 +753,8 @@ func TestVariadic(t *testing.T) {
 	pp2 := &Person{}
 	util.InstanceFromJSON(p2, pp2)
 	pp0.Age, pp1.Age, pp2.Age = 51, 52, 53
-	checkErr(t, m.SaveMany([][]byte{util.JSONFromInstance(pp0), util.JSONFromInstance(pp1), util.JSONFromInstance(pp2)}))
+	err = m.SaveMany([][]byte{util.JSONFromInstance(pp0), util.JSONFromInstance(pp1), util.JSONFromInstance(pp2)})
+	checkErr(t, err)
 	assertPersonInCollection(t, m, util.JSONFromInstance(pp0))
 	assertPersonInCollection(t, m, util.JSONFromInstance(pp1))
 	assertPersonInCollection(t, m, util.JSONFromInstance(pp2))
@@ -796,6 +800,13 @@ func TestGetInstance(t *testing.T) {
 		foundInstance := &Person{}
 		util.InstanceFromJSON(found, foundInstance)
 
+		// Should have a "readonly" _mod/Mod field
+		if foundInstance.Mod == 0 {
+			t.Fatalf(errInvalidInstanceState)
+		}
+		// Update Mod because we know it won't match otherwise
+		newPersonInstance.Mod = foundInstance.Mod
+
 		if !reflect.DeepEqual(newPersonInstance, foundInstance) {
 			t.Fatalf(errInvalidInstanceState)
 		}
@@ -811,6 +822,13 @@ func TestGetInstance(t *testing.T) {
 		foundInstance := &Person{}
 		util.InstanceFromJSON(found, foundInstance)
 
+		// Should have a "readonly" _mod/Mod field
+		if foundInstance.Mod == 0 {
+			t.Fatalf(errInvalidInstanceState)
+		}
+		// Update Mod because we know it won't match otherwise
+		newPersonInstance.Mod = foundInstance.Mod
+
 		if !reflect.DeepEqual(newPersonInstance, foundInstance) {
 			t.Fatalf(errInvalidInstanceState)
 		}
@@ -832,6 +850,95 @@ func TestGetInstance(t *testing.T) {
 	})
 }
 
+func TestModifiedSince(t *testing.T) {
+	t.Parallel()
+	t.Run("WithiSingleCreate", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		c, err := db.NewCollection(CollectionConfig{
+			Name:   "Person",
+			Schema: util.SchemaFromInstance(&Person{}, false),
+		})
+		checkErr(t, err)
+
+		before := time.Now().UnixNano()
+
+		newPerson := util.JSONFromInstance(Person{Name: "Alice", Age: 42})
+		var res []core.InstanceID
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			res, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		instance, err := c.FindByID(res[0])
+		checkErr(t, err)
+
+		p := &Person{}
+		util.InstanceFromJSON(instance, p)
+
+		mods, err := c.ModifiedSince(before)
+		if len(mods) != 1 {
+			t.Fatalf("should have had %d modified instance", 1)
+		}
+		if mods[0] != p.ID {
+			t.Fatalf("should have modfied id %s", p.ID)
+		}
+	})
+	t.Run("WithCreateSaveAndDelete", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		c, err := db.NewCollection(CollectionConfig{
+			Name:   "Person",
+			Schema: util.SchemaFromInstance(&Person{}, false),
+		})
+		checkErr(t, err)
+
+		before := time.Now().UnixNano()
+
+		// Create
+		newPerson := util.JSONFromInstance(Person{Name: "Alice", Age: 42})
+		var res []core.InstanceID
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			res, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		// Save
+		newPerson = util.JSONFromInstance(Person{ID: res[0], Name: "Alice", Age: 51})
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			return txn.Save(newPerson)
+		})
+		checkErr(t, err)
+
+		// Extra
+		newPerson = util.JSONFromInstance(Person{Name: "Bill", Age: 33})
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			_, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		// Delete
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			err = txn.Delete(res[0])
+			return
+		})
+		checkErr(t, err)
+
+		mods, err := c.ModifiedSince(before)
+		if len(mods) != 2 {
+			t.Fatalf("should have had %d modified instance", 2)
+		}
+		if mods[0] != res[0] {
+			t.Fatalf("should have modfied id %s", res[0])
+		}
+	})
+}
+
 func TestVerifyInstance(t *testing.T) {
 	t.Parallel()
 	t.Run("WithoutWriteValidator", func(t *testing.T) {
@@ -947,6 +1054,10 @@ func TestSaveInstance(t *testing.T) {
 		if person.ID != res[0] || person.Age != 42 || person.Name != "Bob" {
 			t.Fatalf(errInvalidInstanceState)
 		}
+		// Should have a "readonly" _mod/Mod field
+		if person.Mod == 0 {
+			t.Fatalf(errInvalidInstanceState)
+		}
 	})
 	t.Run("SaveNonExistant", func(t *testing.T) {
 		t.Parallel()
@@ -959,8 +1070,83 @@ func TestSaveInstance(t *testing.T) {
 		checkErr(t, err)
 
 		p := util.JSONFromInstance(Person{Name: "Alice", Age: 42})
-		if err := m.Save(p); !errors.Is(err, errCantSaveNonExistentInstance) {
-			t.Fatal("shouldn't save non-existent instasnce")
+		if err := m.Save(p); err != nil {
+			t.Fatal("should have saved non-existent instasnce")
+		}
+	})
+}
+
+func TestModTagIncrement(t *testing.T) {
+	t.Parallel()
+	t.Run("Simple", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		c, err := db.NewCollection(CollectionConfig{
+			Name:   "Person",
+			Schema: util.SchemaFromInstance(&Person{}, false),
+		})
+		checkErr(t, err)
+
+		// Specify a zero value mod tag
+		newPerson := util.JSONFromInstance(Person{Name: "Alice", Age: 42, Mod: 0})
+		var res []core.InstanceID
+		err = c.WriteTxn(func(txn *Txn) (err error) {
+			res, err = txn.Create(newPerson)
+			return
+		})
+		checkErr(t, err)
+
+		err = c.WriteTxn(func(txn *Txn) error {
+			instance, err := txn.FindByID(res[0])
+			checkErr(t, err)
+
+			p := &Person{}
+			util.InstanceFromJSON(instance, p)
+
+			// Should have a "readonly" _mod/Mod field that is not zero
+			if p.Mod == 0 {
+				t.Fatalf(errInvalidInstanceState)
+			}
+
+			p.Mod = 0 // Try to make it zero again
+			return txn.Save(util.JSONFromInstance(p))
+		})
+		checkErr(t, err)
+
+		instance, err := c.FindByID(res[0])
+		checkErr(t, err)
+		person := &Person{}
+		util.InstanceFromJSON(instance, person)
+		if person.ID != res[0] || person.Age != 42 || person.Name != "Alice" || person.Mod == 0 {
+			t.Fatalf(errInvalidInstanceState)
+		}
+	})
+	t.Run("WithoutModField", func(t *testing.T) {
+		t.Parallel()
+		db, clean := createTestDB(t)
+		defer clean()
+		m, err := db.NewCollection(CollectionConfig{
+			Name:   "Dog",
+			Schema: util.SchemaFromInstance(&Dog{}, false),
+		})
+		checkErr(t, err)
+
+		p := util.JSONFromInstance(Dog{Name: "Lucas", Comments: []Comment{}})
+		id, err := m.Create(p)
+		checkErr(t, err)
+
+		type WithMod struct {
+			Mod int64 `json:"_mod"`
+		}
+
+		instance, err := m.FindByID(id)
+		checkErr(t, err)
+		dog := &WithMod{}
+		util.InstanceFromJSON(instance, dog)
+		// Should have a "readonly" _mod/Mod field
+		if dog.Mod == 0 {
+			t.Fatalf(errInvalidInstanceState)
 		}
 	})
 }
@@ -995,8 +1181,8 @@ func TestDeleteInstance(t *testing.T) {
 	}
 
 	// Try to delete again
-	if err = c.Delete(res[0]); err != ErrInstanceNotFound {
-		t.Fatalf("cant't delete non-existent instance")
+	if err = c.Delete(res[0]); err != nil {
+		t.Fatalf("should be able to ignore deleting non-existent instances")
 	}
 }
 
@@ -1039,6 +1225,8 @@ func assertPersonInCollection(t *testing.T, c *Collection, personBytes []byte) {
 	checkErr(t, err)
 	p := &Person{}
 	util.InstanceFromJSON(res, p)
+	// Hack to ensure _mod is always updated and equal
+	person.Mod = p.Mod
 	if !reflect.DeepEqual(person, p) {
 		t.Fatalf(errInvalidInstanceState)
 	}
diff --git a/db/db_test.go b/db/db_test.go
index 294e519..d98a792 100644
--- a/db/db_test.go
+++ b/db/db_test.go
@@ -11,7 +11,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/ipfs/go-ipld-format"
+	format "github.com/ipfs/go-ipld-format"
 	"github.com/multiformats/go-multiaddr"
 	ds "github.com/textileio/go-datastore"
 	"github.com/textileio/go-threads/common"
@@ -47,7 +47,8 @@ func TestE2EWithThreads(t *testing.T) {
 	checkErr(t, err)
 	dummyJSON = util.SetJSONID(res, dummyJSON)
 	dummyJSON = util.SetJSONProperty("Counter", 42, dummyJSON)
-	checkErr(t, c1.Save(dummyJSON))
+	err = c1.Save(dummyJSON)
+	checkErr(t, err)
 
 	// Make sure the thread can't be deleted directly
 	err = n1.DeleteThread(context.Background(), id1)
@@ -366,7 +367,8 @@ func runListenersComplexUseCase(t *testing.T, los ...ListenOption) []Action {
 
 	// Collection1 Save i1
 	i1 = util.SetJSONProperty("Name", "Textile0", i1)
-	checkErr(t, c1.Save(i1))
+	err = c1.Save(i1)
+	checkErr(t, err)
 
 	// Collection1 Create i2
 	i2 := util.JSONFromInstance(dummy{ID: "id-i2", Name: "Textile2"})
@@ -390,7 +392,8 @@ func runListenersComplexUseCase(t *testing.T, los ...ListenOption) []Action {
 	// Collection2 Save j1
 	j1 = util.SetJSONProperty("Counter", -1, j1)
 	j1 = util.SetJSONProperty("Name", "Textile33", j1)
-	checkErr(t, c2.Save(j1))
+	err = c2.Save(j1)
+	checkErr(t, err)
 
 	checkErr(t, c1.Delete(i1Ids))
 
diff --git a/integrationtests/foldersync/foldersync_test.go b/integrationtests/foldersync/foldersync_test.go
index a321b5a..a078136 100644
--- a/integrationtests/foldersync/foldersync_test.go
+++ b/integrationtests/foldersync/foldersync_test.go
@@ -149,7 +149,7 @@ func TestNUsersBootstrap(t *testing.T) {
 		randFileSize     int
 		checkSyncedFiles bool
 	}{
-		{totalClients: 2, totalCorePeers: 1, syncTimeout: time.Second * 10},
+		{totalClients: 2, totalCorePeers: 1, syncTimeout: time.Second * 15},
 		{totalClients: 3, totalCorePeers: 1, syncTimeout: time.Second * 30},
 
 		{totalClients: 3, totalCorePeers: 2, syncTimeout: time.Second * 30},
diff --git a/net/api/client/client_test.go b/net/api/client/client_test.go
index 0b61432..b0eb949 100644
--- a/net/api/client/client_test.go
+++ b/net/api/client/client_test.go
@@ -364,7 +364,7 @@ func TestClient_Subscribe(t *testing.T) {
 		if _, err = client1.CreateRecord(context.Background(), info.ID, body2); err != nil {
 			t.Fatal(err)
 		}
-		time.Sleep(time.Second * 2)
+		time.Sleep(time.Second * 10)
 
 		lock.Lock()
 		if rcount != 2 {
diff --git a/net/record_test.go b/net/record_test.go
new file mode 100644
index 0000000..afb300f
--- /dev/null
+++ b/net/record_test.go
@@ -0,0 +1,125 @@
+package net
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/ipfs/go-cid"
+	util "github.com/ipfs/go-ipfs-util"
+	"golang.org/x/exp/rand"
+)
+
+type mockRecord struct {
+	id, prevID cid.Cid
+}
+
+func (r mockRecord) Cid() cid.Cid {
+	return r.id
+}
+
+func (r mockRecord) PrevID() cid.Cid {
+	return r.prevID
+}
+
+func TestNet_RecordSequenceConsistent(t *testing.T) {
+	seqLen := 100
+	seq := generateSequence(cid.Undef, seqLen)
+	recs := newRecordSequence()
+
+	// emulate random order with 2x redundancy
+	seqCpy := make([]linkedRecord, 2*len(seq))
+	copy(seqCpy[:len(seq)], seq)
+	copy(seqCpy[len(seq):], seq)
+	rand.Shuffle(len(seqCpy), func(i, j int) { seqCpy[i], seqCpy[j] = seqCpy[j], seqCpy[i] })
+
+	for _, rec := range seqCpy {
+		recs.Store(rec)
+	}
+
+	collected, ok := recs.List()
+	if !ok {
+		t.Error("cannot reconstruct consistent record sequence")
+	}
+
+	if !equalSequences(collected, seq) {
+		t.Errorf("reconstructed sequence doesn't match original one\noriginal: %s\nrestored: %s",
+			formatSequence(seq), formatSequence(collected))
+	}
+}
+
+func TestNet_RecordSequenceGaps(t *testing.T) {
+	seqLen := 100
+	seq := generateSequence(cid.Undef, seqLen)
+	recs := newRecordSequence()
+
+	// two disjoint subsequences with a single missing record between
+	s1 := seq[:seqLen/2]
+	s2 := seq[seqLen/2+1:]
+
+	for _, rec := range s1 {
+		recs.Store(rec)
+	}
+
+	for _, rec := range s2 {
+		recs.Store(rec)
+	}
+
+	if _, ok := recs.List(); ok {
+		t.Errorf("record sequence with gaps unexpectedly reconstructed")
+	}
+}
+
+func generateSequence(from cid.Cid, size int) []linkedRecord {
+	var (
+		prev = from
+		seq  = make([]linkedRecord, size)
+	)
+
+	for i := 0; i < size; i++ {
+		rec := generateRecord([]byte(fmt.Sprintf("record:%d", i)), prev)
+		seq[i] = rec
+		prev = rec.Cid()
+	}
+
+	return seq
+}
+
+func generateRecord(data []byte, prev cid.Cid) linkedRecord {
+	mh := util.Hash(data)
+	id := cid.NewCidV0(mh)
+	return mockRecord{id: id, prevID: prev}
+}
+
+func equalSequences(s1, s2 []linkedRecord) bool {
+	if len(s1) != len(s2) {
+		return false
+	}
+
+	for i := 0; i < len(s1); i++ {
+		if s1[i].Cid() != s2[i].Cid() || s1[i].PrevID() != s2[i].PrevID() {
+			return false
+		}
+	}
+
+	return true
+}
+
+func formatSequence(seq []linkedRecord) string {
+	var (
+		recs      = make([]string, len(seq))
+		formatCID = func(id cid.Cid) string {
+			if id == cid.Undef {
+				return "Undef"
+			}
+			ir := id.String()
+			return fmt.Sprintf("...%s", ir[len(ir)-6:])
+		}
+	)
+
+	for i, rec := range seq {
+		recs[i] = fmt.Sprintf("(Prev: %s, ID: %s)", formatCID(rec.PrevID()), formatCID(rec.Cid()))
+	}
+
+	return strings.Join(recs, " -> ")
+}