Lots of godoc

This commit is contained in:
Chad Retz 2019-02-26 15:59:44 -06:00
parent 6271b5627b
commit a5611e66b0
15 changed files with 382 additions and 99 deletions

View File

@ -3,7 +3,7 @@
Esgopeta is a Go implementation of the [Gun](https://github.com/amark/gun) distributed graph database. See the Esgopeta is a Go implementation of the [Gun](https://github.com/amark/gun) distributed graph database. See the
[Godoc](https://godoc.org/github.com/cretz/esgopeta/gun) for API details. [Godoc](https://godoc.org/github.com/cretz/esgopeta/gun) for API details.
**WARNING: This is an early proof-of-concept alpha version. Many pieces are not implemented.** **WARNING: This is an early proof-of-concept alpha version. Many pieces are not implemented or don't work.**
Features: Features:

12
gun/doc.go Normal file
View File

@ -0,0 +1,12 @@
/*
Package gun is an implementation of the Gun distributed graph database in Go.
See https://gun.eco for more information on the Gun distributed graph database.
For common use, create a Gun instance via New, use Scoped to arrive at the
desired field, then use Fetch to get/listen to values and/or Put to write
values. A simple example is in the README at https://github.com/cretz/esgopeta.
WARNING: This is an early proof-of-concept alpha version. Many pieces are not
implemented or don't work.
*/
package gun

View File

@ -7,8 +7,11 @@ import (
"time" "time"
) )
// Gun is the main client/server instance for the database.
type Gun struct { type Gun struct {
// Never mutated, always overwritten // currentPeers is the up-to-date peer list. Do not access this without
// using the associated lock. This slice is never mutated, only replaced.
// There are methods to get and change the list.
currentPeers []*Peer currentPeers []*Peer
currentPeersLock sync.RWMutex currentPeersLock sync.RWMutex
@ -28,28 +31,67 @@ type Gun struct {
messageSoulListenersLock sync.RWMutex messageSoulListenersLock sync.RWMutex
} }
// Config is the configuration for a Gun instance.
type Config struct { type Config struct {
PeerURLs []string // PeerURLs is the initial set of peer URLs to connect to. This can be empty
Servers []Server // to not connect to any peers.
Storage Storage PeerURLs []string
SoulGen func() string // Servers is the set of local servers to listen for new peers on. This can
// be empty to not run any server.
Servers []Server
// Storage is the backend storage to locally store data. If not present, a
// NewStorageInMem is created with a value expiration of
// DefaultOldestAllowedStorageValue.
Storage Storage
// SoulGen is a generator for new soul values. If not present,
// DefaultSoulGen is used.
SoulGen func() string
// PeerErrorHandler is called on every error from or concerning a peer. Not
// required.
PeerErrorHandler func(*ErrPeer) PeerErrorHandler func(*ErrPeer)
// PeerSleepOnError is the amount of time we will consider a reconnectable
// peer "bad" on error before attempting reconnect. If 0 (i.e. not set),
// DefaultPeerSleepOnError is used.
PeerSleepOnError time.Duration PeerSleepOnError time.Duration
MyPeerID string // MyPeerID is the identifier given for this peer to whoever asks. If empty
Tracking Tracking // it is set to a random string
MyPeerID string
// Tracking is how seen values are updated when they are seen on the wire.
// When set to TrackingNothing, no seen values will be updated. When set to
// TrackingRequested (the default), only values that are already in storage
// will be updated when seen. When set to TrackingEverything, all values
// seen on the wire will be stored.
Tracking Tracking
} }
// Tracking is what to do when a value update is seen on the wire.
type Tracking int type Tracking int
const ( const (
// TrackingRequested means any value we have already stored will be updated
// when seen.
TrackingRequested Tracking = iota TrackingRequested Tracking = iota
// TrackingNothing means no values will be updated.
TrackingNothing TrackingNothing
// TrackingEverything means every value update is stored.
TrackingEverything TrackingEverything
) )
// DefaultPeerSleepOnError is the default amount of time that an errored
// reconnectable peer will wait until trying to reconnect.
const DefaultPeerSleepOnError = 30 * time.Second const DefaultPeerSleepOnError = 30 * time.Second
// DefaultOldestAllowedStorageValue is the default NewStorageInMem expiration.
const DefaultOldestAllowedStorageValue = 7 * (60 * time.Minute) const DefaultOldestAllowedStorageValue = 7 * (60 * time.Minute)
// New creates a new Gun instance for the given config. The given context is
// used for all peer connection reconnects now/later, all server servings, and
// peer message handling. Therefore it should be kept available at least until
// Close. Callers must still call Close on complete, the completion of the
// context does not automatically do it.
//
// This returns nil with an error on any initial peer connection failure (and
// cleans up any other peers temporarily connected to).
func New(ctx context.Context, config Config) (*Gun, error) { func New(ctx context.Context, config Config) (*Gun, error) {
g := &Gun{ g := &Gun{
currentPeers: make([]*Peer, len(config.PeerURLs)), currentPeers: make([]*Peer, len(config.PeerURLs)),
@ -62,16 +104,25 @@ func New(ctx context.Context, config Config) (*Gun, error) {
messageIDListeners: map[string]chan<- *messageReceived{}, messageIDListeners: map[string]chan<- *messageReceived{},
messageSoulListeners: map[string]chan<- *messageReceived{}, messageSoulListeners: map[string]chan<- *messageReceived{},
} }
// Create all the peers // Set config defaults
sleepOnError := config.PeerSleepOnError if g.storage == nil {
if sleepOnError == 0 { g.storage = NewStorageInMem(DefaultOldestAllowedStorageValue)
sleepOnError = DefaultPeerSleepOnError
} }
if g.soulGen == nil {
g.soulGen = DefaultSoulGen
}
if g.peerSleepOnError == 0 {
g.peerSleepOnError = DefaultPeerSleepOnError
}
if g.myPeerID == "" {
g.myPeerID = randString(9)
}
// Create all the peers
var err error var err error
for i := 0; i < len(config.PeerURLs) && err == nil; i++ { for i := 0; i < len(config.PeerURLs) && err == nil; i++ {
peerURL := config.PeerURLs[i] peerURL := config.PeerURLs[i]
newConn := func() (PeerConn, error) { return NewPeerConn(ctx, peerURL) } newConn := func() (PeerConn, error) { return NewPeerConn(ctx, peerURL) }
if g.currentPeers[i], err = newPeer(peerURL, newConn, sleepOnError); err != nil { if g.currentPeers[i], err = newPeer(peerURL, newConn, g.peerSleepOnError); err != nil {
err = fmt.Errorf("Failed connecting to peer %v: %v", peerURL, err) err = fmt.Errorf("Failed connecting to peer %v: %v", peerURL, err)
} }
} }
@ -84,33 +135,27 @@ func New(ctx context.Context, config Config) (*Gun, error) {
} }
return nil, err return nil, err
} }
// Set defaults
if g.storage == nil {
g.storage = NewStorageInMem(DefaultOldestAllowedStorageValue)
}
if g.soulGen == nil {
g.soulGen = DefaultSoulGen
}
if g.myPeerID == "" {
g.myPeerID = randString(9)
}
// Start receiving from peers // Start receiving from peers
for _, peer := range g.currentPeers { for _, peer := range g.currentPeers {
go g.startReceiving(peer) go g.startReceiving(ctx, peer)
} }
// Start all the servers // Start all the servers
go g.startServers(config.Servers) go g.startServers(ctx, config.Servers)
return g, nil return g, nil
} }
func (g *Gun) Scoped(ctx context.Context, key string, children ...string) *Scoped { // Scoped returns a scoped instance to the given field and children for reading
s := newScoped(g, nil, key) // and writing. This is the equivalent of calling the Gun JS API "get" function
// (sans callback) for each field/child.
func (g *Gun) Scoped(ctx context.Context, field string, children ...string) *Scoped {
s := newScoped(g, nil, field)
if len(children) > 0 { if len(children) > 0 {
s = s.Scoped(ctx, children[0], children[1:]...) s = s.Scoped(ctx, children[0], children[1:]...)
} }
return s return s
} }
// Close stops all peers and servers and closes the underlying storage.
func (g *Gun) Close() error { func (g *Gun) Close() error {
var errs []error var errs []error
for _, p := range g.peers() { for _, p := range g.peers() {
@ -188,11 +233,10 @@ func (g *Gun) send(ctx context.Context, msg *Message, ignorePeer *Peer) <-chan *
return ch return ch
} }
func (g *Gun) startReceiving(peer *Peer) { func (g *Gun) startReceiving(ctx context.Context, peer *Peer) {
// TDO: some kind of overall context is probably needed ctx, cancelFn := context.WithCancel(ctx)
ctx, cancelFn := context.WithCancel(context.TODO())
defer cancelFn() defer cancelFn()
for !peer.Closed() { for !peer.Closed() && ctx.Err() == nil {
// We might not be able receive because peer is sleeping from // We might not be able receive because peer is sleeping from
// an error happened within or a just-before send error. // an error happened within or a just-before send error.
if ok, msgs, err := peer.receive(ctx); !ok { if ok, msgs, err := peer.receive(ctx); !ok {

View File

@ -2,6 +2,7 @@ package gun
import "encoding/json" import "encoding/json"
// Message is the JSON-encodable message that Gun peers send to each other.
type Message struct { type Message struct {
Ack string `json:"@,omitempty"` Ack string `json:"@,omitempty"`
ID string `json:"#,omitempty"` ID string `json:"#,omitempty"`
@ -16,6 +17,7 @@ type Message struct {
Err string `json:"err,omitempty"` Err string `json:"err,omitempty"`
} }
// MessageGetRequest is the format for Message.Get.
type MessageGetRequest struct { type MessageGetRequest struct {
Soul string `json:"#,omitempty"` Soul string `json:"#,omitempty"`
Field string `json:".,omitempty"` Field string `json:".,omitempty"`
@ -23,7 +25,10 @@ type MessageGetRequest struct {
type messageReceived struct { type messageReceived struct {
*Message *Message
peer *Peer
peer *Peer // storedPuts are the souls and their fields that have been stored by
// another part of the code. This is useful if the main instance stores
// something it sees, there's no need for the message listener to do so as
// well.
storedPuts map[string][]string storedPuts map[string][]string
} }

View File

@ -7,6 +7,10 @@ import (
"strconv" "strconv"
) )
// DefaultSoulGen is the default soul generator. It uses the current time in
// MS as the first part of the string. However if that MS was already used in
// this process, the a guaranteed-process-unique-nano-level time is added to
// the first part. The second part is a random string.
func DefaultSoulGen() string { func DefaultSoulGen() string {
ms, uniqueNum := timeNowUniqueUnix() ms, uniqueNum := timeNowUniqueUnix()
s := strconv.FormatInt(ms, 36) s := strconv.FormatInt(ms, 36)
@ -16,11 +20,16 @@ func DefaultSoulGen() string {
return s + randString(12) return s + randString(12)
} }
// Node is a JSON-encodable representation of a Gun node which is a set of
// scalar values by name and metadata about those values.
type Node struct { type Node struct {
// Metadata is the metadata for this node.
Metadata Metadata
// Values is the set of values (including null) keyed by the field name.
Values map[string]Value Values map[string]Value
} }
// MarshalJSON implements encoding/json.Marshaler for Node.
func (n *Node) MarshalJSON() ([]byte, error) { func (n *Node) MarshalJSON() ([]byte, error) {
// Just put it all in a map and then encode it // Just put it all in a map and then encode it
toEnc := make(map[string]interface{}, len(n.Values)+1) toEnc := make(map[string]interface{}, len(n.Values)+1)
@ -31,6 +40,7 @@ func (n *Node) MarshalJSON() ([]byte, error) {
return json.Marshal(toEnc) return json.Marshal(toEnc)
} }
// UnmarshalJSON implements encoding/json.Unmarshaler for Node.
func (n *Node) UnmarshalJSON(b []byte) error { func (n *Node) UnmarshalJSON(b []byte) error {
dec := json.NewDecoder(bytes.NewReader(b)) dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber() dec.UseNumber()
@ -60,15 +70,24 @@ func (n *Node) UnmarshalJSON(b []byte) error {
} }
} }
// Metadata is the soul of a node and the state of its values.
type Metadata struct { type Metadata struct {
Soul string `json:"#,omitempty"` // Soul is the unique identifier of this node.
Soul string `json:"#,omitempty"`
// State is the conflict state value for each node field.
State map[string]State `json:">,omitempty"` State map[string]State `json:">,omitempty"`
} }
// Value is the common interface implemented by all possible Gun values. The
// possible values of Value are nil and instances of ValueNumber, ValueString,
// ValueBool, and ValueRelation. They can all be marshaled into JSON.
type Value interface { type Value interface {
nodeValue() nodeValue()
} }
// ValueDecodeJSON decodes a single Value from JSON. For the given JSON decoder
// and last read token, this decodes a Value. The decoder needs to have ran
// UseNumber. Unrecognized values are errors.
func ValueDecodeJSON(token json.Token, dec *json.Decoder) (Value, error) { func ValueDecodeJSON(token json.Token, dec *json.Decoder) (Value, error) {
switch token := token.(type) { switch token := token.(type) {
case nil: case nil:
@ -102,22 +121,39 @@ func ValueDecodeJSON(token json.Token, dec *json.Decoder) (Value, error) {
} }
} }
// ValueString is a representation of a string Value.
type ValueString string type ValueString string
func (ValueString) nodeValue() {} func (ValueString) nodeValue() {}
func (v ValueString) String() string { return string(v) }
// ValueNumber is a representation of a number Value. It is typed as a string
// similar to how encoding/json.Number works since it can overflow numeric
// types.
type ValueNumber string type ValueNumber string
func (ValueNumber) nodeValue() {} func (ValueNumber) nodeValue() {}
func (v ValueNumber) String() string { return string(v) }
// Float64 returns the number as a float64.
func (v ValueNumber) Float64() (float64, error) { return strconv.ParseFloat(string(v), 64) }
// Int64 returns the number as an int64.
func (v ValueNumber) Int64() (int64, error) { return strconv.ParseInt(string(v), 10, 64) }
// ValueBool is a representation of a bool Value.
type ValueBool bool type ValueBool bool
func (ValueBool) nodeValue() {} func (ValueBool) nodeValue() {}
// ValueRelation is a representation of a relation Value. The value is the soul
// of the linked node. It has a custom JSON encoding.
type ValueRelation string type ValueRelation string
func (ValueRelation) nodeValue() {} func (ValueRelation) nodeValue() {}
func (v ValueRelation) String() string { return string(v) }
func (n ValueRelation) MarshalJSON() ([]byte, error) { // MarshalJSON implements encoding/json.Marshaler fr ValueRelation.
return json.Marshal(map[string]string{"#": string(n)}) func (v ValueRelation) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{"#": string(v)})
} }

View File

@ -8,13 +8,19 @@ import (
"time" "time"
) )
// ErrPeer is an error specific to a peer.
type ErrPeer struct { type ErrPeer struct {
Err error // Err is the error.
Err error
// Peer is the peer the error relates to.
Peer *Peer Peer *Peer
} }
func (e *ErrPeer) Error() string { return fmt.Sprintf("Error on peer %v: %v", e.Peer, e.Err) } func (e *ErrPeer) Error() string { return fmt.Sprintf("Error on peer %v: %v", e.Peer, e.Err) }
// Peer is a known peer to Gun. It has a single connection. Some peers are
// "reconnectable" which means due to failure, they may be in a "bad" state
// awaiting reconnection.
type Peer struct { type Peer struct {
name string name string
newConn func() (PeerConn, error) newConn func() (PeerConn, error)
@ -35,8 +41,15 @@ func newPeer(name string, newConn func() (PeerConn, error), sleepOnErr time.Dura
return p, nil return p, nil
} }
// ID is the identifier of the peer as given by the peer. It is empty if the
// peer wasn't asked for or didn't give an ID.
func (p *Peer) ID() string { return p.id } func (p *Peer) ID() string { return p.id }
// Name is the name of the peer which is usually the URL.
func (p *Peer) Name() string { return p.name }
// String is a string representation of the peer including whether it's
// connected.
func (p *Peer) String() string { func (p *Peer) String() string {
id := "" id := ""
if p.id != "" { if p.id != "" {
@ -69,7 +82,8 @@ func (p *Peer) reconnect() (err error) {
return return
} }
// Can be nil peer if currently bad or closed // Conn is the underlying PeerConn. This can be nil if the peer is currently
// "bad" or closed.
func (p *Peer) Conn() PeerConn { func (p *Peer) Conn() PeerConn {
p.connLock.Lock() p.connLock.Lock()
defer p.connLock.Unlock() defer p.connLock.Unlock()
@ -110,9 +124,8 @@ func (p *Peer) send(ctx context.Context, msg *Message, moreMsgs ...*Message) (ok
if err = conn.Send(ctx, updatedMsg, updatedMoreMsgs...); err != nil { if err = conn.Send(ctx, updatedMsg, updatedMoreMsgs...); err != nil {
p.markConnErrored(conn) p.markConnErrored(conn)
return false, err return false, err
} else {
return true, nil
} }
return true, nil
} }
func (p *Peer) receive(ctx context.Context) (ok bool, msgs []*Message, err error) { func (p *Peer) receive(ctx context.Context) (ok bool, msgs []*Message, err error) {
@ -126,6 +139,7 @@ func (p *Peer) receive(ctx context.Context) (ok bool, msgs []*Message, err error
} }
} }
// Close closes the peer and the connection is connected.
func (p *Peer) Close() error { func (p *Peer) Close() error {
p.connLock.Lock() p.connLock.Lock()
defer p.connLock.Unlock() defer p.connLock.Unlock()
@ -138,19 +152,30 @@ func (p *Peer) Close() error {
return err return err
} }
// Closed is whether the peer is closed.
func (p *Peer) Closed() bool { func (p *Peer) Closed() bool {
p.connLock.Lock() p.connLock.Lock()
defer p.connLock.Unlock() defer p.connLock.Unlock()
return p.connCurrent == nil && !p.connBad return p.connCurrent == nil && !p.connBad
} }
// PeerConn is a single peer connection.
type PeerConn interface { type PeerConn interface {
// Send sends the given message (and maybe others) to the peer. The context
// governs just this send.
Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error
// Receive waits for the next message (or set of messages if sent at once)
// from a peer. The context can be used to control a timeout.
Receive(ctx context.Context) ([]*Message, error) Receive(ctx context.Context) ([]*Message, error)
// RemoteURL is the URL this peer is connected via.
RemoteURL() string RemoteURL() string
// Close closes this connection.
Close() error Close() error
} }
// PeerURLSchemes is the map that maps URL schemes to factory functions to
// create the connection. Currently "http" and "https" simply defer to "ws" and
// "wss" respectively. "ws" and "wss" use DialPeerConnWebSocket.
var PeerURLSchemes map[string]func(context.Context, *url.URL) (PeerConn, error) var PeerURLSchemes map[string]func(context.Context, *url.URL) (PeerConn, error)
func init() { func init() {
@ -176,6 +201,7 @@ func init() {
} }
} }
// NewPeerConn connects to a peer for the given URL.
func NewPeerConn(ctx context.Context, peerURL string) (PeerConn, error) { func NewPeerConn(ctx context.Context, peerURL string) (PeerConn, error) {
if parsedURL, err := url.Parse(peerURL); err != nil { if parsedURL, err := url.Parse(peerURL); err != nil {
return nil, err return nil, err

View File

@ -7,6 +7,7 @@ import (
"sync" "sync"
) )
// Scoped is a contextual, namespaced field to read or write.
type Scoped struct { type Scoped struct {
gun *Gun gun *Gun
@ -32,10 +33,21 @@ func newScoped(gun *Gun, parent *Scoped, field string) *Scoped {
} }
} }
// ErrNotObject occurs when a put or a fetch is attempted as a child of an
// existing, non-relation value.
var ErrNotObject = errors.New("Scoped value not an object") var ErrNotObject = errors.New("Scoped value not an object")
// ErrLookupOnTopLevel occurs when a put or remote fetch is attempted on
// a top-level field.
var ErrLookupOnTopLevel = errors.New("Cannot do put/lookup on top level") var ErrLookupOnTopLevel = errors.New("Cannot do put/lookup on top level")
// Empty string if doesn't exist, ErrNotObject if self or parent not an object // Soul returns the current soul of this value relation. It returns a cached
// value if called before. Otherwise, it does a FetchOne to get the value
// and return its soul if a relation. If any parent is not a relation or this
// value exists and is not a relation, ErrNotObject is returned. If this field
// doesn't exist yet, an empty string is returned with no error. Otherwise,
// the soul of the relation is returned. The context can be used to timeout the
// fetch.
func (s *Scoped) Soul(ctx context.Context) (string, error) { func (s *Scoped) Soul(ctx context.Context) (string, error) {
if cachedSoul := s.cachedSoul(); cachedSoul != "" { if cachedSoul := s.cachedSoul(); cachedSoul != "" {
return cachedSoul, nil return cachedSoul, nil
@ -68,8 +80,11 @@ func (s *Scoped) setCachedSoul(val ValueRelation) bool {
return true return true
} }
func (s *Scoped) Scoped(ctx context.Context, key string, children ...string) *Scoped { // Scoped returns a scoped instance to the given field and children for reading
ret := newScoped(s.gun, s, key) // and writing. This is the equivalent of calling the Gun JS API "get" function
// (sans callback) for each field/child.
func (s *Scoped) Scoped(ctx context.Context, field string, children ...string) *Scoped {
ret := newScoped(s.gun, s, field)
for _, child := range children { for _, child := range children {
ret = newScoped(s.gun, ret, child) ret = newScoped(s.gun, ret, child)
} }

View File

@ -12,18 +12,39 @@ type fetchResultListener struct {
receivedMessages chan *messageReceived receivedMessages chan *messageReceived
} }
// FetchResult is a result of a fetch.
type FetchResult struct { type FetchResult struct {
// This can be a context error on cancelation // Err is any error on fetch, local or remote. This can be a context error
Err error // if the fetch's context completes. This is nil on successful fetch.
//
// This may be ErrLookupOnTopLevel for a remote fetch of a top-level field.
// This may be ErrNotObject if the field is a child of a non-relation value.
Err error
// Field is the name of the field that was fetched. It is a convenience
// value for the scope's field this was originally called on.
Field string Field string
// Nil if the value doesn't exist, exists and is nil, or there's an error // Value is the fetched value. This will be nil if Err is not nil. This will
Value Value // also be nil if a peer said the value does not exist. This will also be
State State // This can be 0 for errors or top-level value relations // nil if the value exists but is nil. Use ValueExists to distinguish
// between the last two cases.
Value Value
// State is the conflict state of the value. It may be 0 for errors. It may
// also be 0 if this is a top-level field.
State State
// ValueExists is true if there is no error and the fetched value, nil or
// not, does exist.
ValueExists bool ValueExists bool
// Nil when local and sometimes on error // Peer is the peer this result is for. This is nil for results from local
// storage. This may be nil on error.
Peer *Peer Peer *Peer
} }
// FetchOne fetches a single result, trying local first. It is a shortcut for
// calling FetchOneLocal and if it doesn't exist falling back to FetchOneRemote.
// This should not be called on a top-level field. The context can be used to
// timeout the wait.
//
// This is the equivalent of the Gun JS API "once" function.
func (s *Scoped) FetchOne(ctx context.Context) *FetchResult { func (s *Scoped) FetchOne(ctx context.Context) *FetchResult {
// Try local before remote // Try local before remote
if r := s.FetchOneLocal(ctx); r.Err != nil || r.ValueExists { if r := s.FetchOneLocal(ctx); r.Err != nil || r.ValueExists {
@ -32,6 +53,8 @@ func (s *Scoped) FetchOne(ctx context.Context) *FetchResult {
return s.FetchOneRemote(ctx) return s.FetchOneRemote(ctx)
} }
// FetchOneLocal gets a local value from storage. For top-level fields, it
// simply returns the field name as a relation.
func (s *Scoped) FetchOneLocal(ctx context.Context) *FetchResult { func (s *Scoped) FetchOneLocal(ctx context.Context) *FetchResult {
// If there is no parent, this is just the relation // If there is no parent, this is just the relation
if s.parent == nil { if s.parent == nil {
@ -50,6 +73,10 @@ func (s *Scoped) FetchOneLocal(ctx context.Context) *FetchResult {
return r return r
} }
// FetchOneRemote fetches a single result from the first peer that responds. It
// will not look in storage first. This will error if called for a top-level
// field. This is a shortcut for calling FetchRemote, waiting for a single
// value, then calling FetchDone. The context can be used to timeout the wait.
func (s *Scoped) FetchOneRemote(ctx context.Context) *FetchResult { func (s *Scoped) FetchOneRemote(ctx context.Context) *FetchResult {
if s.parent == nil { if s.parent == nil {
return &FetchResult{Err: ErrLookupOnTopLevel, Field: s.field} return &FetchResult{Err: ErrLookupOnTopLevel, Field: s.field}
@ -59,6 +86,15 @@ func (s *Scoped) FetchOneRemote(ctx context.Context) *FetchResult {
return <-ch return <-ch
} }
// Fetch fetches and listens for updates on the field and sends them to the
// resulting channel. This will error if called for a top-level field. The
// resulting channel is closed on Gun close, context completion, or when
// FetchDone is called with it. Users should ensure one of the three happen in a
// reasonable timeframe to stop listening and prevent leaks. This is a shortcut
// for FetchOneLocal (but doesn't send to channel if doesn't exist) followed by
// FetchRemote.
//
// This is the equivalent of the Gun JS API "on" function.
func (s *Scoped) Fetch(ctx context.Context) <-chan *FetchResult { func (s *Scoped) Fetch(ctx context.Context) <-chan *FetchResult {
ch := make(chan *FetchResult, 1) ch := make(chan *FetchResult, 1)
if s.parent == nil { if s.parent == nil {
@ -73,11 +109,15 @@ func (s *Scoped) Fetch(ctx context.Context) <-chan *FetchResult {
return ch return ch
} }
// FetchRemote fetches and listens for updates on a field only from peers, not
// via storage. This will error if called for a top-level field. The resulting
// channel is closed on Gun close, context completion, or when FetchDone is
// called with it. Users should ensure one of the three happen in a reasonable
// timeframe to stop listening and prevent leaks.
func (s *Scoped) FetchRemote(ctx context.Context) <-chan *FetchResult { func (s *Scoped) FetchRemote(ctx context.Context) <-chan *FetchResult {
ch := make(chan *FetchResult, 1) ch := make(chan *FetchResult, 1)
if s.parent == nil { if s.parent == nil {
ch <- &FetchResult{Err: ErrLookupOnTopLevel, Field: s.field} ch <- &FetchResult{Err: ErrLookupOnTopLevel, Field: s.field}
close(ch)
} else { } else {
go s.fetchRemote(ctx, ch) go s.fetchRemote(ctx, ch)
} }
@ -176,6 +216,9 @@ func (s *Scoped) fetchRemote(ctx context.Context, ch chan *FetchResult) {
}() }()
} }
// FetchDone is called with a channel returned from Fetch or FetchRemote to stop
// listening and close the channel. It returns true if it actually stopped
// listening or false if it wasn't listening.
func (s *Scoped) FetchDone(ch <-chan *FetchResult) bool { func (s *Scoped) FetchDone(ch <-chan *FetchResult) bool {
s.fetchResultListenersLock.Lock() s.fetchResultListenersLock.Lock()
l := s.fetchResultListeners[ch] l := s.fetchResultListeners[ch]

View File

@ -11,22 +11,53 @@ type putResultListener struct {
receivedMessages chan *messageReceived receivedMessages chan *messageReceived
} }
// PutResult is either an acknowledgement or an error for a put.
type PutResult struct { type PutResult struct {
// Err is any error on put, local or remote. This can be a context error
// if the put's context completes. This is nil on successful put.
//
// This may be ErrLookupOnTopLevel for a remote fetch of a top-level field.
// This may be ErrNotObject if the field is a child of a non-relation value.
Err error Err error
// Nil on error or local put success // Field is the name of the field that was put. It is a convenience value
// for the scope's field this was originally called on.
Field string
// Peer is the peer this result is for. This is nil for results from local
// storage. This may be nil on error.
Peer *Peer Peer *Peer
} }
type PutOption interface{} // PutOption is the base interface for all options that can be passed to Put.
type PutOption interface {
putOption()
}
type putOptionStoreLocalOnly struct{} type putOptionStoreLocalOnly struct{}
func PutOptionStoreLocalOnly() PutOption { return putOptionStoreLocalOnly{} } func (putOptionStoreLocalOnly) putOption() {}
// PutOptionStoreLocalOnly makes Put only store locally and then be done.
var PutOptionStoreLocalOnly PutOption = putOptionStoreLocalOnly{}
type putOptionFailWithoutParent struct{} type putOptionFailWithoutParent struct{}
func PutOptionFailWithoutParent() PutOption { return putOptionFailWithoutParent{} } func (putOptionFailWithoutParent) putOption() {}
// PutOptionFailWithoutParent makes Put fail if it would need to lazily create
// parent relations.
var PutOptionFailWithoutParent PutOption = putOptionFailWithoutParent{}
// Put puts a value on the field in local storage. It also sends the put to all
// peers unless the PutOptionStoreLocalOnly option is present. Each
// acknowledgement or error will be sent to the resulting channel. Unless the
// PutOptionFailWithoutParent option is present, this will lazily create all
// parent relations that do not already exist. This will error if called for a
// top-level field. The resulting channel is closed on Gun close, context
// completion, or when PutDone is called with it. Users should ensure one of the
// three happen in a reasonable timeframe to stop listening for acks and prevent
// leaks.
//
// This is the equivalent of the Gun JS API "put" function.
func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *PutResult { func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *PutResult {
// Collect the options // Collect the options
storeLocalOnly := false storeLocalOnly := false
@ -47,19 +78,16 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
} }
if len(parents) == 0 { if len(parents) == 0 {
ch <- &PutResult{Err: ErrLookupOnTopLevel} ch <- &PutResult{Err: ErrLookupOnTopLevel}
close(ch)
return ch return ch
} }
// Ask for the soul on the last parent. What this will do is trigger // Ask for the soul on the last parent. What this will do is trigger
// lazy soul fetch up the chain. Then we can go through and find who doesn't have a // lazy soul fetch up the chain. Then we can go through and find who doesn't have a
// cached soul, create one, and store locally. // cached soul, create one, and store locally.
if soul, err := parents[len(parents)-1].Soul(ctx); err != nil { if soul, err := parents[len(parents)-1].Soul(ctx); err != nil {
ch <- &PutResult{Err: ErrLookupOnTopLevel} ch <- &PutResult{Err: ErrLookupOnTopLevel, Field: s.field}
close(ch)
return ch return ch
} else if soul == "" && failWithoutParent { } else if soul == "" && failWithoutParent {
ch <- &PutResult{Err: fmt.Errorf("Parent not present but required")} ch <- &PutResult{Err: fmt.Errorf("Parent not present but required"), Field: s.field}
close(ch)
return ch return ch
} }
// Now for every parent that doesn't have a cached soul we create one and // Now for every parent that doesn't have a cached soul we create one and
@ -88,12 +116,10 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
// TODO: Should I not store until the very end just in case it errors halfway // TODO: Should I not store until the very end just in case it errors halfway
// though? There are no standard cases where it should fail. // though? There are no standard cases where it should fail.
if _, err := s.gun.storage.Put(ctx, prevParentSoul, parent.field, ValueRelation(parentCachedSoul), currState, false); err != nil { if _, err := s.gun.storage.Put(ctx, prevParentSoul, parent.field, ValueRelation(parentCachedSoul), currState, false); err != nil {
ch <- &PutResult{Err: err} ch <- &PutResult{Err: err, Field: s.field}
close(ch)
return ch return ch
} else if !parent.setCachedSoul(ValueRelation(parentCachedSoul)) { } else if !parent.setCachedSoul(ValueRelation(parentCachedSoul)) {
ch <- &PutResult{Err: fmt.Errorf("Concurrent cached soul set")} ch <- &PutResult{Err: fmt.Errorf("Concurrent cached soul set"), Field: s.field}
close(ch)
return ch return ch
} }
} }
@ -101,14 +127,12 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
} }
// Now that we've setup all the parents, we can do this store locally // Now that we've setup all the parents, we can do this store locally
if _, err := s.gun.storage.Put(ctx, prevParentSoul, s.field, val, currState, false); err != nil { if _, err := s.gun.storage.Put(ctx, prevParentSoul, s.field, val, currState, false); err != nil {
ch <- &PutResult{Err: err} ch <- &PutResult{Err: err, Field: s.field}
close(ch)
return ch return ch
} }
// We need an ack for local store and stop if local only // We need an ack for local store and stop if local only
ch <- &PutResult{} ch <- &PutResult{Field: s.field}
if storeLocalOnly { if storeLocalOnly {
close(ch)
return ch return ch
} }
// Now, we begin the remote storing // Now, we begin the remote storing
@ -130,14 +154,14 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
ch <- &PutResult{Err: ctx.Err()} ch <- &PutResult{Err: ctx.Err(), Field: s.field}
s.PutDone(ch) s.PutDone(ch)
return return
case msg, ok := <-msgCh: case msg, ok := <-msgCh:
if !ok { if !ok {
return return
} }
r := &PutResult{Peer: msg.peer} r := &PutResult{Field: s.field, Peer: msg.peer}
if msg.Err != "" { if msg.Err != "" {
r.Err = fmt.Errorf("Remote error: %v", msg.Err) r.Err = fmt.Errorf("Remote error: %v", msg.Err)
} else if msg.OK != 1 { } else if msg.OK != 1 {
@ -151,14 +175,18 @@ func (s *Scoped) Put(ctx context.Context, val Value, opts ...PutOption) <-chan *
go func() { go func() {
for peerErr := range s.gun.send(ctx, req, nil) { for peerErr := range s.gun.send(ctx, req, nil) {
safePutResultSend(ch, &PutResult{ safePutResultSend(ch, &PutResult{
Err: peerErr.Err, Err: peerErr.Err,
Peer: peerErr.Peer, Field: s.field,
Peer: peerErr.Peer,
}) })
} }
}() }()
return ch return ch
} }
// PutDone is called with a channel returned from Put to stop
// listening for acks and close the channel. It returns true if it actually
// stopped listening or false if it wasn't listening.
func (s *Scoped) PutDone(ch <-chan *PutResult) bool { func (s *Scoped) PutDone(ch <-chan *PutResult) bool {
s.putResultListenersLock.Lock() s.putResultListenersLock.Lock()
l := s.putResultListeners[ch] l := s.putResultListeners[ch]

View File

@ -4,14 +4,19 @@ import (
"context" "context"
) )
// Server is the interface implemented by servers.
type Server interface { type Server interface {
Serve() error // Hangs forever // Serve is called by Gun to start the server. It should not return until
// an error occurs or Close is called.
Serve() error
// Accept is called to wait for the next peer connection or if an error
// occurs while trying.
Accept() (PeerConn, error) Accept() (PeerConn, error)
// Close is called by Gun to stop and close this server.
Close() error Close() error
} }
func (g *Gun) startServers(servers []Server) { func (g *Gun) startServers(ctx context.Context, servers []Server) {
ctx := context.Background()
ctx, g.serversCancelFn = context.WithCancel(ctx) ctx, g.serversCancelFn = context.WithCancel(ctx)
for _, server := range servers { for _, server := range servers {
// TODO: log error? // TODO: log error?

View File

@ -6,27 +6,57 @@ import (
"time" "time"
) )
type State uint64 // State represents the conflict state of this value. It is usually the Unix
// time in milliseconds.
type State float64 // TODO: what if larger?
func StateNow() State { return State(timeNowUnixMs()) } // StateNow is the current machine state (i.e. current Unix time in ms).
func StateNow() State {
// TODO: Should I use timeNowUniqueUnix or otherwise set decimal spots to disambiguate?
return State(timeNowUnixMs())
}
// StateFromTime converts a time to a State (i.e. converts to Unix ms).
func StateFromTime(t time.Time) State { return State(timeToUnixMs(t)) } func StateFromTime(t time.Time) State { return State(timeToUnixMs(t)) }
// ConflictResolution is how to handle two values for the same field.
type ConflictResolution int type ConflictResolution int
const ( const (
// ConflictResolutionNeverSeenUpdate occurs when there is no existing value.
// It means an update should always occur.
ConflictResolutionNeverSeenUpdate ConflictResolution = iota ConflictResolutionNeverSeenUpdate ConflictResolution = iota
// ConflictResolutionTooFutureDeferred occurs when the update is after our
// current machine state. It means the update should be deferred.
ConflictResolutionTooFutureDeferred ConflictResolutionTooFutureDeferred
// ConflictResolutionOlderHistorical occurs when the update happened before
// the existing value's last update. It means it can be noted, but the
// update should be discarded.
ConflictResolutionOlderHistorical ConflictResolutionOlderHistorical
// ConflictResolutionNewerUpdate occurs when the update happened after last
// update but is not beyond ur current machine state. It means the update
// should overwrite.
ConflictResolutionNewerUpdate ConflictResolutionNewerUpdate
// ConflictResolutionSameKeep occurs when the update happened at the same
// time and it is lexically not the one chosen. It means the update should
// be discarded.
ConflictResolutionSameKeep ConflictResolutionSameKeep
// ConflictResolutionSameUpdate occurs when the update happened at the same
// time and it is lexically the one chosen. It means the update should
// overwrite.
ConflictResolutionSameUpdate ConflictResolutionSameUpdate
) )
// IsImmediateUpdate returns true for ConflictResolutionNeverSeenUpdate,
// ConflictResolutionNewerUpdate, and ConflictResolutionSameUpdate
func (c ConflictResolution) IsImmediateUpdate() bool { func (c ConflictResolution) IsImmediateUpdate() bool {
return c == ConflictResolutionNeverSeenUpdate || c == ConflictResolutionNewerUpdate || c == ConflictResolutionSameUpdate return c == ConflictResolutionNeverSeenUpdate || c == ConflictResolutionNewerUpdate || c == ConflictResolutionSameUpdate
} }
// ConflictResolve checks the existing val/state, new val/state, and the current
// machine state to choose what to do with the update. Note, the existing val
// should always exist meaning it will never return
// ConflictResolutionNeverSeenUpdate.
func ConflictResolve(existingVal Value, existingState State, newVal Value, newState State, sysState State) ConflictResolution { func ConflictResolve(existingVal Value, existingState State, newVal Value, newState State, sysState State) ConflictResolution {
// Existing gunjs impl serializes to JSON first to do lexical comparisons, so we will too // Existing gunjs impl serializes to JSON first to do lexical comparisons, so we will too
if sysState < newState { if sysState < newState {

View File

@ -3,21 +3,37 @@ package gun
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"sync" "sync"
"time" "time"
) )
// ErrStorageNotFound is returned by Storage.Get and sometimes Storage.Put when
// the field doesn't exist.
var ErrStorageNotFound = errors.New("Not found") var ErrStorageNotFound = errors.New("Not found")
// Storage is the interface that storage adapters must implement.
type Storage interface { type Storage interface {
// Get obtains the value (which can be nil) and state from storage for the
// given field. If the field does not exist, this errors with
// ErrStorageNotFound.
Get(ctx context.Context, parentSoul, field string) (Value, State, error) Get(ctx context.Context, parentSoul, field string) (Value, State, error)
// Put sets the value (which can be nil) and state in storage for the given
// field if the conflict resolution says it should (see ConflictResolve). It
// also returns the conflict resolution. If onlyIfExists is true and the
// field does not exist, this errors with ErrStorageNotFound. Otherwise, if
// the resulting resolution is an immediate update, it is done. If the
// resulting resolution is deferred for the future, it is scheduled for then
// but is not even attempted if context is completed or storage is closed.
Put(ctx context.Context, parentSoul, field string, val Value, state State, onlyIfExists bool) (ConflictResolution, error) Put(ctx context.Context, parentSoul, field string, val Value, state State, onlyIfExists bool) (ConflictResolution, error)
// Close closes this storage and disallows future gets or puts.
Close() error Close() error
} }
type storageInMem struct { type storageInMem struct {
values map[parentSoulAndField]*valueWithState values map[parentSoulAndField]*valueWithState
valueLock sync.RWMutex valueLock sync.RWMutex
closed bool // Do not mutate outside of valueLock
purgeCancelFn context.CancelFunc purgeCancelFn context.CancelFunc
} }
@ -28,6 +44,9 @@ type valueWithState struct {
state State state State
} }
// NewStorageInMem creates an in-memory storage that automatically purges
// values that are older than the given oldestAllowed. If oldestAllowed is 0,
// it keeps all values forever.
func NewStorageInMem(oldestAllowed time.Duration) Storage { func NewStorageInMem(oldestAllowed time.Duration) Storage {
s := &storageInMem{values: map[parentSoulAndField]*valueWithState{}} s := &storageInMem{values: map[parentSoulAndField]*valueWithState{}}
// Start the purger // Start the purger
@ -60,7 +79,9 @@ func NewStorageInMem(oldestAllowed time.Duration) Storage {
func (s *storageInMem) Get(ctx context.Context, parentSoul, field string) (Value, State, error) { func (s *storageInMem) Get(ctx context.Context, parentSoul, field string) (Value, State, error) {
s.valueLock.RLock() s.valueLock.RLock()
defer s.valueLock.RUnlock() defer s.valueLock.RUnlock()
if vs := s.values[parentSoulAndField{parentSoul, field}]; vs == nil { if s.closed {
return nil, 0, fmt.Errorf("Storage closed")
} else if vs := s.values[parentSoulAndField{parentSoul, field}]; vs == nil {
return nil, 0, ErrStorageNotFound return nil, 0, ErrStorageNotFound
} else { } else {
return vs.val, vs.state, nil return vs.val, vs.state, nil
@ -74,7 +95,9 @@ func (s *storageInMem) Put(
defer s.valueLock.Unlock() defer s.valueLock.Unlock()
key, newVs := parentSoulAndField{parentSoul, field}, &valueWithState{val, state} key, newVs := parentSoulAndField{parentSoul, field}, &valueWithState{val, state}
sysState := StateNow() sysState := StateNow()
if existingVs := s.values[key]; existingVs == nil && onlyIfExists { if s.closed {
return 0, fmt.Errorf("Storage closed")
} else if existingVs := s.values[key]; existingVs == nil && onlyIfExists {
return 0, ErrStorageNotFound return 0, ErrStorageNotFound
} else if existingVs == nil { } else if existingVs == nil {
confRes = ConflictResolutionNeverSeenUpdate confRes = ConflictResolutionNeverSeenUpdate
@ -84,9 +107,13 @@ func (s *storageInMem) Put(
if confRes == ConflictResolutionTooFutureDeferred { if confRes == ConflictResolutionTooFutureDeferred {
// Schedule for 100ms past when it's deferred to // Schedule for 100ms past when it's deferred to
time.AfterFunc(time.Duration(state-sysState)*time.Millisecond+100, func() { time.AfterFunc(time.Duration(state-sysState)*time.Millisecond+100, func() {
// TODO: should I check whether closed? s.valueLock.RLock()
closed := s.closed
s.valueLock.RUnlock()
// TODO: what to do w/ error? // TODO: what to do w/ error?
s.Put(ctx, parentSoul, field, val, state, onlyIfExists) if !closed && ctx.Err() == nil {
s.Put(ctx, parentSoul, field, val, state, onlyIfExists)
}
}) })
} else if confRes.IsImmediateUpdate() { } else if confRes.IsImmediateUpdate() {
s.values[key] = newVs s.values[key] = newVs
@ -98,5 +125,8 @@ func (s *storageInMem) Close() error {
if s.purgeCancelFn != nil { if s.purgeCancelFn != nil {
s.purgeCancelFn() s.purgeCancelFn()
} }
s.valueLock.Lock()
defer s.valueLock.Unlock()
s.closed = true
return nil return nil
} }

View File

@ -16,7 +16,7 @@ func TestGunGetSimple(t *testing.T) {
randStr := randString(30) randStr := randString(30)
// Write w/ JS // Write w/ JS
ctx.runJSWithGun(` ctx.runJSWithGun(`
gun.get('esgopeta-test').get('TestGunGetSimple').get('some-key').put('` + randStr + `', ack => { gun.get('esgopeta-test').get('TestGunGetSimple').get('some-field').put('` + randStr + `', ack => {
if (ack.err) { if (ack.err) {
console.error(ack.err) console.error(ack.err)
process.exit(1) process.exit(1)
@ -28,13 +28,13 @@ func TestGunGetSimple(t *testing.T) {
g := ctx.newGunConnectedToGunJS() g := ctx.newGunConnectedToGunJS()
defer g.Close() defer g.Close()
// Make sure we got back the same value // Make sure we got back the same value
r := g.Scoped(ctx, "esgopeta-test", "TestGunGetSimple", "some-key").FetchOne(ctx) r := g.Scoped(ctx, "esgopeta-test", "TestGunGetSimple", "some-field").FetchOne(ctx)
ctx.Require.NoError(r.Err) ctx.Require.NoError(r.Err)
ctx.Require.Equal(gun.ValueString(randStr), r.Value.(gun.ValueString)) ctx.Require.Equal(gun.ValueString(randStr), r.Value.(gun.ValueString))
// Do it again with the JS server closed since it should fetch from memory // Do it again with the JS server closed since it should fetch from memory
serverCancelFn() serverCancelFn()
ctx.debugf("Asking for key again") ctx.debugf("Asking for field again")
r = g.Scoped(ctx, "esgopeta-test", "TestGunGetSimple", "some-key").FetchOne(ctx) r = g.Scoped(ctx, "esgopeta-test", "TestGunGetSimple", "some-field").FetchOne(ctx)
ctx.Require.NoError(r.Err) ctx.Require.NoError(r.Err)
ctx.Require.Equal(gun.ValueString(randStr), r.Value.(gun.ValueString)) ctx.Require.Equal(gun.ValueString(randStr), r.Value.(gun.ValueString))
} }
@ -44,11 +44,11 @@ func TestGunGetSimpleRemote(t *testing.T) {
ctx, cancelFn := newContext(t) ctx, cancelFn := newContext(t)
defer cancelFn() defer cancelFn()
remoteURL := ctx.prepareRemoteGunServer(defaultRemoteGunServerURL) remoteURL := ctx.prepareRemoteGunServer(defaultRemoteGunServerURL)
randKey, randVal := "key-"+randString(30), gun.ValueString(randString(30)) randField, randVal := "field-"+randString(30), gun.ValueString(randString(30))
// Write w/ JS // Write w/ JS
ctx.debugf("Writing value") ctx.debugf("Writing value")
ctx.runJSWithGunURL(remoteURL, ` ctx.runJSWithGunURL(remoteURL, `
gun.get('esgopeta-test').get('TestGunGetSimpleRemote').get('`+randKey+`').put('`+string(randVal)+`', ack => { gun.get('esgopeta-test').get('TestGunGetSimpleRemote').get('`+randField+`').put('`+string(randVal)+`', ack => {
if (ack.err) { if (ack.err) {
console.error(ack.err) console.error(ack.err)
process.exit(1) process.exit(1)
@ -61,7 +61,7 @@ func TestGunGetSimpleRemote(t *testing.T) {
g := ctx.newGunConnectedToGunServer(remoteURL) g := ctx.newGunConnectedToGunServer(remoteURL)
defer g.Close() defer g.Close()
// Make sure we got back the same value // Make sure we got back the same value
r := g.Scoped(ctx, "esgopeta-test", "TestGunGetSimpleRemote", randKey).FetchOne(ctx) r := g.Scoped(ctx, "esgopeta-test", "TestGunGetSimpleRemote", randField).FetchOne(ctx)
ctx.Require.NoError(r.Err) ctx.Require.NoError(r.Err)
ctx.Require.Equal(randVal, r.Value) ctx.Require.Equal(randVal, r.Value)
} }
@ -74,7 +74,7 @@ func TestGunPutSimple(t *testing.T) {
g := ctx.newGunConnectedToGunJS() g := ctx.newGunConnectedToGunJS()
defer g.Close() defer g.Close()
// Just wait for two acks (one local, one remote) // Just wait for two acks (one local, one remote)
ch := g.Scoped(ctx, "esgopeta-test", "TestGunPutSimple", "some-key").Put(ctx, gun.ValueString(randStr)) ch := g.Scoped(ctx, "esgopeta-test", "TestGunPutSimple", "some-field").Put(ctx, gun.ValueString(randStr))
// TODO: test local is null peer and remote is non-null // TODO: test local is null peer and remote is non-null
r := <-ch r := <-ch
ctx.Require.NoError(r.Err) ctx.Require.NoError(r.Err)
@ -82,7 +82,7 @@ func TestGunPutSimple(t *testing.T) {
ctx.Require.NoError(r.Err) ctx.Require.NoError(r.Err)
// Get from JS // Get from JS
out := ctx.runJSWithGun(` out := ctx.runJSWithGun(`
gun.get('esgopeta-test').get('TestGunPutSimple').get('some-key').once(data => { gun.get('esgopeta-test').get('TestGunPutSimple').get('some-field').once(data => {
console.log(data) console.log(data)
process.exit(0) process.exit(0)
}) })
@ -94,17 +94,17 @@ func TestGunPubSubSimpleRemote(t *testing.T) {
ctx, cancelFn := newContext(t) ctx, cancelFn := newContext(t)
defer cancelFn() defer cancelFn()
remoteURL := ctx.prepareRemoteGunServer(defaultRemoteGunServerURL) remoteURL := ctx.prepareRemoteGunServer(defaultRemoteGunServerURL)
randKey, randVal := "key-"+randString(30), gun.ValueString(randString(30)) randField, randVal := "field-"+randString(30), gun.ValueString(randString(30))
// Start a fetcher // Start a fetcher
ctx.debugf("Starting fetcher") ctx.debugf("Starting fetcher")
fetchGun := ctx.newGunConnectedToGunServer(remoteURL) fetchGun := ctx.newGunConnectedToGunServer(remoteURL)
defer fetchGun.Close() defer fetchGun.Close()
fetchCh := fetchGun.Scoped(ctx, "esgopeta-test", "TestGunPubSubSimpleRemote", randKey).Fetch(ctx) fetchCh := fetchGun.Scoped(ctx, "esgopeta-test", "TestGunPubSubSimpleRemote", randField).Fetch(ctx)
// Now put it from another instance // Now put it from another instance
ctx.debugf("Putting data") ctx.debugf("Putting data")
putGun := ctx.newGunConnectedToGunServer(remoteURL) putGun := ctx.newGunConnectedToGunServer(remoteURL)
defer putGun.Close() defer putGun.Close()
putScope := putGun.Scoped(ctx, "esgopeta-test", "TestGunPubSubSimpleRemote", randKey) putScope := putGun.Scoped(ctx, "esgopeta-test", "TestGunPubSubSimpleRemote", randField)
putScope.Put(ctx, randVal) putScope.Put(ctx, randVal)
ctx.debugf("Checking fetcher") ctx.debugf("Checking fetcher")
// See that the fetch got the value // See that the fetch got the value

View File

@ -17,7 +17,7 @@ func TestGunJS(t *testing.T) {
defer cancelFn() defer cancelFn()
randStr := randString(30) randStr := randString(30)
ctx.runJSWithGun(` ctx.runJSWithGun(`
gun.get('esgopeta-test').get('TestGunJS').get('some-key').put('` + randStr + `', ack => { gun.get('esgopeta-test').get('TestGunJS').get('some-field').put('` + randStr + `', ack => {
if (ack.err) { if (ack.err) {
console.error(ack.err) console.error(ack.err)
process.exit(1) process.exit(1)
@ -26,7 +26,7 @@ func TestGunJS(t *testing.T) {
}) })
`) `)
out := ctx.runJSWithGun(` out := ctx.runJSWithGun(`
gun.get('esgopeta-test').get('TestGunJS').get('some-key').once(data => { gun.get('esgopeta-test').get('TestGunJS').get('some-field').once(data => {
console.log(data) console.log(data)
process.exit(0) process.exit(0)
}) })

View File

@ -19,7 +19,9 @@ type serverWebSocket struct {
serveErrCh chan error serveErrCh chan error
} }
// NewServerWebSocket creates a new Server for the given HTTP server and given upgrader.
func NewServerWebSocket(server *http.Server, upgrader *websocket.Upgrader) Server { func NewServerWebSocket(server *http.Server, upgrader *websocket.Upgrader) Server {
// TODO: wait, what if they already have a server and want to control serve, close, and handler?
if upgrader == nil { if upgrader == nil {
upgrader = &websocket.Upgrader{} upgrader = &websocket.Upgrader{}
} }
@ -64,23 +66,27 @@ func (s *serverWebSocket) Close() error {
return s.server.Close() return s.server.Close()
} }
// PeerConnWebSocket implements PeerConn for a websocket connection.
type PeerConnWebSocket struct { type PeerConnWebSocket struct {
Underlying *websocket.Conn Underlying *websocket.Conn
WriteLock sync.Mutex WriteLock sync.Mutex
} }
func DialPeerConnWebSocket(ctx context.Context, peerUrl *url.URL) (*PeerConnWebSocket, error) { // DialPeerConnWebSocket opens a peer websocket connection.
conn, _, err := websocket.DefaultDialer.DialContext(ctx, peerUrl.String(), nil) func DialPeerConnWebSocket(ctx context.Context, peerURL *url.URL) (*PeerConnWebSocket, error) {
conn, _, err := websocket.DefaultDialer.DialContext(ctx, peerURL.String(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewPeerConnWebSocket(conn), nil return NewPeerConnWebSocket(conn), nil
} }
// NewPeerConnWebSocket wraps an existing websocket connection.
func NewPeerConnWebSocket(underlying *websocket.Conn) *PeerConnWebSocket { func NewPeerConnWebSocket(underlying *websocket.Conn) *PeerConnWebSocket {
return &PeerConnWebSocket{Underlying: underlying} return &PeerConnWebSocket{Underlying: underlying}
} }
// Send implements PeerConn.Send.
func (p *PeerConnWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error { func (p *PeerConnWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...*Message) error {
// If there are more, send all as an array of JSON strings, otherwise just the msg // If there are more, send all as an array of JSON strings, otherwise just the msg
var toWrite interface{} var toWrite interface{}
@ -115,6 +121,7 @@ func (p *PeerConnWebSocket) Send(ctx context.Context, msg *Message, moreMsgs ...
} }
} }
// Receive implements PeerConn.Receive.
func (p *PeerConnWebSocket) Receive(ctx context.Context) ([]*Message, error) { func (p *PeerConnWebSocket) Receive(ctx context.Context) ([]*Message, error) {
bytsCh := make(chan []byte, 1) bytsCh := make(chan []byte, 1)
errCh := make(chan error, 1) errCh := make(chan error, 1)
@ -153,10 +160,12 @@ func (p *PeerConnWebSocket) Receive(ctx context.Context) ([]*Message, error) {
} }
} }
// RemoteURL implements PeerConn.RemoteURL.
func (p *PeerConnWebSocket) RemoteURL() string { func (p *PeerConnWebSocket) RemoteURL() string {
return fmt.Sprintf("http://%v", p.Underlying.RemoteAddr()) return fmt.Sprintf("http://%v", p.Underlying.RemoteAddr())
} }
// Close implements PeerConn.Close.
func (p *PeerConnWebSocket) Close() error { func (p *PeerConnWebSocket) Close() error {
return p.Underlying.Close() return p.Underlying.Close()
} }