diff --git a/docs/CloudDocument.html b/docs/CloudDocument.html new file mode 100644 index 0000000..276612d --- /dev/null +++ b/docs/CloudDocument.html @@ -0,0 +1,3 @@ +Interface: CloudDocument
On this page

CloudDocument

Represents a cloud document with cloud change notifications, multi-tenant ownership and ganular access controls

Members

owner :IdObj

CloudDocument owner as an IdObj

Type:

Methods

(async) acl() → {Acl}

Get this document's Acl

Returns:
Type: 
Acl

(async) grantAccess(action, actor, field, allowed)

Allow access to this document or a subfield

Parameters:
NameTypeDefaultDescription
actionstring

CRUFL operation

actorIdObj

Actor object

fieldstring

Document subfield

allowedstringtrue

Allow/deny named actor access

@dataparty/api
\ No newline at end of file diff --git a/docs/LokiParty.html b/docs/LokiParty.html new file mode 100644 index 0000000..d4eacaf --- /dev/null +++ b/docs/LokiParty.html @@ -0,0 +1,3 @@ +Class: LokiParty
On this page

LokiParty

@dataparty/api
\ No newline at end of file diff --git a/docs/ServiceHost.html b/docs/ServiceHost.html new file mode 100644 index 0000000..bd66f71 --- /dev/null +++ b/docs/ServiceHost.html @@ -0,0 +1,3 @@ +Class: ServiceHost
On this page

ServiceHost

@dataparty/api
\ No newline at end of file diff --git a/docs/bouncer_index.js.html b/docs/bouncer_index.js.html new file mode 100644 index 0000000..d0ba5b4 --- /dev/null +++ b/docs/bouncer_index.js.html @@ -0,0 +1,16 @@ +Source: bouncer/index.js
On this page

bouncer_index.js

/**
+ * @module Db
+ */
+
+exports.ICrufler= require('./icrufler')
+exports.IDb= require('./idb')
+exports.ISchema= require('./ischema')
+exports.MongoQuery= require('./mongo-query')
+exports.LokiDb= require('./db/loki-db')
+exports.TingoDb= require('./db/tingo-db')
+exports.ZangoDb= require('./db/zango-db')
+
+
+
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_ble-socket.js.html b/docs/comms_ble-socket.js.html new file mode 100644 index 0000000..6aebf4c --- /dev/null +++ b/docs/comms_ble-socket.js.html @@ -0,0 +1,264 @@ +Source: comms/ble-socket.js
On this page

comms_ble-socket.js

'use strict'
+
+const debug = require('debug')('dataparty.comms.ble-peer')
+const EventEmitter = require('eventemitter3')
+
+const BLEMessage = require('./ble/BLEMessage')
+const BLEOp = require('./ble/BLEOp')
+// const bluetooth = (navigator) navigator.bluetooth
+
+/**
+ * A simple BLE socket
+ * ⚠️ Warning: This class maybe significantly refactored in future releases
+ * 
+ * @class module:Comms.BLEPeerClient
+ * @extends EventEmitter
+ * @link module:Comms
+ * @see https://webbluetoothcg.github.io/web-bluetooth
+ * @param {BleDevice} bleDevice A connected BLE device. See BLEPeerClient.requestDevice()
+ */
+
+class BLEPeerClient extends EventEmitter {
+  constructor(bleDevice){
+    super()
+    this.autoReconnect = true
+    this.reconnectFails = 0
+    this.reconnectDelay = 500
+    this.reconnectMaxTries = 5
+    this.reconnectTimer = undefined
+
+    this.rxBuffer = {} // indexed on command sequence -> Message
+
+    /* */
+    this.bleDevice = bleDevice
+    this.info = undefined
+
+    this.characteristics = {
+      owner: undefined,
+      actor: undefined,
+      tx: undefined,
+      rx: undefined
+    }
+
+    this.bleDevice.addEventListener('gattserverdisconnected', this.handleGATTDisconnected.bind(this))
+  }
+
+  static get MTU(){ return 20 }
+  static get DATAPARTY_SERVICE_UUID(){ return 0x31337 }
+  static get OWNER_ID_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000001-abcd-42d0-bc79-df8168b55f04') }
+  static get ACTOR_ID_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000002-abcd-42d0-bc79-df8168b55f04') }
+  static get RX_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000003-abcd-42d0-bc79-df8168b55f04') }
+  static get TX_CHARACTERISTIC_UUID(){ return BluetoothUUID.getCharacteristic('00000004-abcd-42d0-bc79-df8168b55f04') }
+
+  static buf2hex(buffer){
+    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('')
+  }
+
+  static parseActor(data){
+    const dataArr = new Uint8Array(data.buffer)
+    const actorTypeId = parseInt(dataArr.slice(0, 2))
+    const idStr = BLEPeerClient.buf2hex(dataArr.slice(1, dataArr.length))
+
+    const ACTORS = [undefined, 'user', 'team', 'org', 'device', 'app']
+
+    return {
+      type: ACTORS[actorTypeId] || undefined,
+      id: idStr
+    }
+  }
+
+  handleGATTDisconnected(){
+    debug('GATT disconnected', new Date())
+
+    if (this.autoReconnect){
+      this.resume()
+    }
+  }
+
+  async resume(){
+
+    debug('resuming', new Date())
+    return new Promise((resolve, reject) => {
+      this.start()
+        .then(resolve)
+        .catch((err) => {
+          this.reconnectFails += 1
+
+          if (this.reconnectFails >= this.reconnectMaxTries){
+            debug('max-reconnect', err)
+            return reject(new Error('max-reconnect'))
+          }
+
+          const delay = this.reconnectDelay * Math.pow(2, this.reconnectFails)
+          debug('start failed (error,', err, ') retying in', delay, 'ms')
+
+          this.reconnectTimer = setTimeout(() => {
+            this.resume().then(resolve).catch(reject)
+          }, delay)
+        })
+
+    })
+  }
+
+  onRx(event){
+    debug('onRx', event.target.value.buffer)
+
+    debug('raw value', event.target.value)
+    const input = new Uint8Array(event.target.value.buffer)
+
+    debug('input', input)
+
+    const header = BLEMessage.parseHeader(input)
+
+    let buffer = this.rxBuffer[ header.seq ]
+    if (!buffer){
+
+      buffer = BLEMessage.fromPacket(input)
+      this.rxBuffer[ header.seq ] = buffer
+
+    } else {
+
+      buffer.parsePacket(input)
+
+    }
+
+    if (buffer.rxComplete){
+      debug('onRx complete ', header.seq)
+      debug('buffer', buffer)
+      this.emit('message', buffer)
+      this.emit('message:' + header.seq, buffer)
+    }
+
+  }
+
+  async transmit(arr){
+    return this.characteristics.tx.writeValue(arr)
+  }
+
+  async send(obj){
+    debug('sending', obj)
+    const msg = new BLEMessage({msg: obj})
+
+    debug(msg)
+
+    const packets = []
+
+    for (let i = 0; i < msg.packetCount; i++){
+      await this.transmit(msg.getPacket(i))
+    }
+    return msg
+  }
+
+  async run(obj){
+    const op = new BLEOp(obj, this)
+
+    return op.run().then(response => {
+      debug('op got respone')
+      return response
+    })
+  }
+
+  async start(autoreconnect = true){
+    this.autoReconnect = autoreconnect
+
+    try {
+      await this.bleDevice.gatt.connect()
+
+      if (!this.bleDevice.gatt.connected){
+        debug('warning GATT not connected')
+      }
+
+      debug('GATT connected', new Date())
+      const primaryService = await this.bleDevice.gatt.getPrimaryService(BLEPeerClient.DATAPARTY_SERVICE_UUID)
+
+      const primaryCharacteristics = await primaryService.getCharacteristics()
+      debug('PRIMARY SERVICE', primaryCharacteristics.length, primaryCharacteristics)
+
+      debug('\t found primary service')
+      this.characteristics.owner = await primaryService.getCharacteristic(BLEPeerClient.OWNER_ID_CHARACTERISTIC_UUID)
+      debug('\t found owner', this.characteristics.owner)
+
+      this.characteristics.actor = await primaryService.getCharacteristic(BLEPeerClient.ACTOR_ID_CHARACTERISTIC_UUID)
+      debug('\t found actor')
+
+      this.characteristics.tx = await primaryService.getCharacteristic(BLEPeerClient.TX_CHARACTERISTIC_UUID)
+      debug('\t found tx')
+
+      this.characteristics.rx = await primaryService.getCharacteristic(BLEPeerClient.RX_CHARACTERISTIC_UUID)
+      debug('\t found rx')
+
+      this.characteristics.rx.addEventListener('characteristicvaluechanged', this.onRx.bind(this))
+      this.characteristics.rx.startNotifications()
+      debug('\t started rx notifications')
+
+      const owner = await this.characteristics.owner.readValue()
+      debug('\t read owner')
+
+      const actor = await this.characteristics.actor.readValue()
+      debug('\t read actor')
+
+      // let decoder = new TextDecoder('utf-8')
+
+      this.info = {
+        owner: BLEPeerClient.parseActor(owner),
+        actor: BLEPeerClient.parseActor(actor)
+      }
+
+      debug(this.info)
+      this.reconnectFails = 0
+      return this
+    } catch (error){
+      debug('exception in bluetooth :(', error)
+      debug(error)
+      debug(error.stack)
+      if(this.reconnectFails == 0){
+        return this.resume()
+      }
+      return Promise.reject(error)
+    }
+  }
+
+  stop(){
+    this.autoReconnect = false
+
+    if (!this.bleDevice.gatt.connected){
+      debug('gatt already disconnected')
+      return
+    }
+
+    debug('disconnecting gatt')
+    this.bleDevice.gatt.disconnect()
+  }
+
+  static requestDevice(shortId){
+
+    const filters = []
+
+    if (!shortId){
+
+      filters.push({
+        services: [BLEPeerClient.DATAPARTY_SERVICE_UUID],
+        namePrefix: 'DataParty'
+      })
+
+    } else {
+
+      filters.push({
+        services: [BLEPeerClient.DATAPARTY_SERVICE_UUID],
+        name: 'DataParty-' + shortId
+      })
+
+    }
+
+    return navigator.bluetooth.requestDevice({ filters: filters }).then(device => {
+      debug('requested device')
+      debug(device)
+      return new BLEPeerClient(device)
+    })
+  }
+}
+
+module.exports = BLEPeerClient
+
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_i2p-socket-comms.js.html b/docs/comms_i2p-socket-comms.js.html new file mode 100644 index 0000000..8397ab7 --- /dev/null +++ b/docs/comms_i2p-socket-comms.js.html @@ -0,0 +1,130 @@ +Source: comms/i2p-socket-comms.js
On this page

comms_i2p-socket-comms.js

const debug = require('debug')('dataparty.comms.i2psocket')
+const debugShim = require('debug')('dataparty.comms.i2psocket-shim')
+
+const SAM = require('@diva.exchange/i2p-sam')
+const EventEmitter = require('eventemitter3')
+
+
+const PeerComms = require('./peer-comms')
+
+
+class I2pStreamShim extends EventEmitter {
+  constructor(stream){
+    super()
+    this.stream = stream
+
+    this.stream.on('data',data=>{
+      this.emit('data', data)
+    })
+
+    this.stream.on('error',err=>{
+      this.emit('error', err)
+    })
+
+    this.stream.once('close',()=>{
+      this._isConnected = false
+      this.emit('close')
+    })
+
+    this.stream.once('stream',()=>{
+      this._isConnected = true
+      debugShim('shim open')
+      setTimeout(()=>{this.emit('connect')}, 1)
+    })
+
+
+    if(this.stream.hasStream){
+      this._isConnected = true
+      debugShim('has stream')
+      setTimeout(()=>{this.emit('connect')}, 1)
+    }
+  }
+
+  get isConnected(){
+    return this._isConnected
+  }
+
+  async connect(){
+    debugShim('connecting to ', this.stream.destination)
+    return await this.stream.connect()
+  }
+
+  close(){
+    this._closed = true
+    this.stream.close()
+  }
+
+  destroy(){
+    if(!this._closed){
+      this.close()
+    }
+  }
+
+  send(val){ this.stream.send(val) }
+
+}
+
+/**
+ * A peer comms based on i2p using the SAM module from diva.exchange
+ * 
+ * @class module:Comms.I2pSocketComms
+ * @implements {module:Comms.PeerComms}
+ * @link module:Comms
+ * @see https://geti2p.net/en/
+ * @param {string} destination An i2p destination uri
+ * @param {SAM.I2pSamStream} stream Optional, already connected SAM.I2pSamStream
+ */
+
+class I2pSocketComms extends PeerComms {
+  constructor({destination, stream, samHost, remoteIdentity, host, party, ...options}){
+    super({remoteIdentity, host, party, ...options})
+
+    this.stream = stream
+    this.destination = destination
+    this.samHost = samHost || { 
+      host: '127.0.0.1',
+      portTCP: 7656
+    }
+
+    if(this.host && !this.stream){
+      throw new Error('existing connection expected')
+    }
+
+    if(!this.host && (!this.destination && !this.stream)){
+      throw new Error('destination or existing stream expected')
+    }
+  }
+
+
+  async socketInit(){
+    debug('init')
+    
+    if(!this.host && !this.stream){
+      debug('opening client connection to -',this.destination, ' via SAM', JSON.stringify(this.samHost,null,2))
+
+      this.stream = await SAM.createStream({
+        sam: this.samHost,
+        stream: { destination: this.destination }
+      })
+
+    } else if(this.stream){
+
+      debug('using existing stream', this.stream.getPublicKey())
+
+    }
+
+    this.socket = new I2pStreamShim(this.stream)
+
+    if( !this.socket.isConnected ){
+
+      await this.socket.connect()
+
+    }
+  }
+}
+
+
+module.exports = I2pSocketComms
+
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_index.js.html b/docs/comms_index.js.html new file mode 100644 index 0000000..51650b2 --- /dev/null +++ b/docs/comms_index.js.html @@ -0,0 +1,21 @@ +Source: comms/index.js
On this page

comms_index.js


+/**
+ *  @module Comms 
+ */
+exports.BLEMessage = require('./ble/BLEMessage')
+exports.BLEOp = require('./ble/BLEOp')
+exports.AuthOp = require('./op/auth-op')
+exports.AuthError = require('../errors/auth-error')
+exports.RestComms = require('./rest-comms')
+exports.ISocketComms = require('./isocket-comms')
+exports.LoopbackComms = require('./loopback-comms')
+exports.LoopbackChannel = require('./loopback-channel')
+exports.LoopbackChannelPort = require('./loopback-channel-port')
+exports.PeerComms = require('./peer-comms')
+exports.WebsocketComms = require('./websocket-comms')
+exports.RTCSocketComms = require('./rtc-socket-comms')
+exports.I2pSocketComms = require('./i2p-socket-comms')
+exports.SocketOp = require('./isocket-comms')
+exports.WebsocketOp = require('./websocket-op')
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_isocket-comms.js.html b/docs/comms_isocket-comms.js.html new file mode 100644 index 0000000..f8b2b5d --- /dev/null +++ b/docs/comms_isocket-comms.js.html @@ -0,0 +1,161 @@ +Source: comms/isocket-comms.js
On this page

comms_isocket-comms.js

'use strict'
+
+const debug = require('debug')('dataparty.comms.socketcomms')
+const EventEmitter = require('eventemitter3')
+
+const {Message, Routines} = require('@dataparty/crypto')
+
+const AuthOp = require('./op/auth-op')
+const RosShim = require('./ros-shim')
+
+
+/**
+ * @interface module:Comms.ISocketComms
+ * @link module:Comms
+ * @extends EventEmitter
+ */
+
+class ISocketComms extends EventEmitter {
+    constructor({session, uri, party, remoteIdentity, discoverRemoteIdentity}){
+        super()
+        this.uri = uri
+        this.session = session
+        this.remoteIdentity = remoteIdentity
+        this.discoverRemoteIdentity = discoverRemoteIdentity
+
+        this.party = party //used for access to primary identity
+
+        this.connected = false
+        this.authed = undefined
+        
+        this._opId = Math.round(Math.random()*65536)
+
+        this.socket = undefined
+
+        this._ros = undefined
+    }
+
+    get opId(){
+        return this._opId++
+    }
+
+    authorized(){
+        return new Promise((resolve,reject)=>{
+            if(this.authed){
+                return resolve()
+            }
+
+            this.once('open', resolve)
+            this.once('close', reject)
+        }).then(()=>{
+            return this
+        })
+    }
+
+    close(){
+        debug('Client closing connection')
+        this.socket.close()
+    }
+
+    onclose(){
+        this.authed = false
+        this.connected = false
+        debug('Server closed connection')
+        this.emit('close')
+    }
+
+    onopen(){
+        this.authed = false
+        this.connected = true
+        debug('socket open!')
+
+        let op = new AuthOp(this)
+
+        op.run().then((status)=>{
+            debug(status)
+            debug('authed')
+            this.emit('open')
+            this.authed = true
+        }).catch(error=>{
+            this.authed = false
+            debug('auth error', error)
+            this.emit('close')
+        })
+    }
+
+    decrypt(reply, sender){
+        const replyObj = JSON.parse(reply.data)
+        let dataPromise = new Promise((resolve, reject)=>{
+            if(replyObj.enc && replyObj.sig){
+              let msg = new Message(replyObj)
+      
+              return resolve(msg.decrypt(this.party._identity).then(content=>{
+                const senderPub = Routines.extractPublicKeys(msg.enc)
+                debug('sender', sender, '\tdiscover', this.discoverRemoteIdentity)
+                if(this.discoverRemoteIdentity && !sender){
+                    debug('discovered remote identity', senderPub)
+                    this.remoteIdentity = {
+                        key: {
+                            public: senderPub
+                        }
+                    }
+                    sender = this.remoteIdentity
+                }
+                debug(`senderPub - ${senderPub}`)
+      
+                if(senderPub.box != sender.key.public.box || senderPub.sign != sender.key.public.sign){
+                  return Promise.reject('TRUST - reply is not from expected remote')
+                }
+
+                debug('decrypted data')
+                return content
+              }))
+            }
+      
+            reject( Promise.reject('TRUST - reply is not encrypted') )
+          })
+      
+        return dataPromise
+    }
+
+    onmessage(message){
+        debug('onmessage', message)
+        let comm = this
+        this.decrypt(message, this.remoteIdentity).then(msg=>{
+            debug('decrypted msg = ', msg)
+            debug(msg.id)
+
+            if(msg.op != 'publish'){
+                debug('emit id')
+                comm.emit(msg.id, msg)
+            } else {
+                debug('emit message')
+                comm.emit('message', msg)
+            }
+        })
+    }
+
+    send(input){
+        debug('send - ', input)
+
+        const content = new Message({msg: input})
+
+        return content.encrypt(this.party._identity, this.remoteIdentity.key)
+            .then(JSON.stringify)
+            .then(this.socket.send.bind(this.socket))
+
+    }
+
+    get ros(){
+        if(!this._ros){
+            this._ros = new RosShim(this)
+            this._ros.connect()
+        }
+
+        return this._ros
+    }
+}
+
+module.exports = ISocketComms
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_loopback-channel.js.html b/docs/comms_loopback-channel.js.html new file mode 100644 index 0000000..c0427f6 --- /dev/null +++ b/docs/comms_loopback-channel.js.html @@ -0,0 +1,26 @@ +Source: comms/loopback-channel.js
On this page

comms_loopback-channel.js

const debug = require('debug')('dataparty.comms.loopback-channel')
+const EventEmitter = require("eventemitter3")
+
+
+const LoopbackChannelPort = require('./loopback-channel-port')
+
+/**
+ * @class module:Comms.LoopbackChannel
+ * @implements {module:Comms.ISocketComms}
+ * @extends {module:Comms.ISocketComms}
+ * @link module:Comms
+ */
+module.exports = class LoopbackChannel {
+  constructor(){
+
+    //! The first channel peer
+    this.port1 = new LoopbackChannelPort(undefined, '1')
+
+    //! The second channel peer
+    this.peer2 = new LoopbackChannelPort(this.port1, '2')
+
+    this.port1.peer = this.port2
+  }
+}
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_loopback-comms.js.html b/docs/comms_loopback-comms.js.html new file mode 100644 index 0000000..342efce --- /dev/null +++ b/docs/comms_loopback-comms.js.html @@ -0,0 +1,37 @@ +Source: comms/loopback-comms.js
On this page

comms_loopback-comms.js

const debug = require('debug')('dataparty.comms.loopback-comms')
+
+const PeerComms = require('./peer-comms')
+const LoopbackSocket = require('./loopback-socket')
+
+const AUTH_TIMEOUT_MS = 3000
+
+
+/**
+ * @class module:Comms.LoopbackComms
+ * @implements {module:Comms.PeerComms}
+ * @link module:Comms
+ * @param {module:Comms.LoopbackChannelPort} channel 
+ */
+class LoopbackComms extends PeerComms {
+
+  constructor({remoteIdentity, host, party, channel, ...options}){
+    super({remoteIdentity, host, party, ...options})
+
+    this.channel = channel
+  }
+
+  async socketInit(){
+    debug('init')
+    this.socket = new LoopbackSocket(this.channel)
+  }
+
+  async socketStart(){
+    debug('start')
+    this.socket.start()
+  }
+}
+
+
+module.exports = LoopbackComms
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_loopback-socket.js.html b/docs/comms_loopback-socket.js.html new file mode 100644 index 0000000..0f36627 --- /dev/null +++ b/docs/comms_loopback-socket.js.html @@ -0,0 +1,99 @@ +Source: comms/loopback-socket.js
On this page

comms_loopback-socket.js

const debug = require('debug')('dataparty.comms.loopback-socket')
+const EventEmitter = require('eventemitter3')
+
+/**
+ * @class module:Comms.LoopbackSocket
+ * @link module:Comms
+ * @extends EventEmitter
+ */
+
+module.exports = class LoopbackSocket extends EventEmitter {
+  constructor(channel){
+    super()
+    this.channel = channel
+    this.ready_local = false
+    this.ready_remote = false
+    this.ready = false
+    this.closed = true
+
+    this.channel.on('connected', this.onconnected.bind(this))
+    this.channel.on('data', this.ondata.bind(this))
+    this.channel.on('close', this.onclose.bind(this))
+
+    
+  }
+
+  start(){
+    debug('start')
+    //this.channel.post('connected', true)
+    this.ready_local = true
+    this.checkConnected()
+  }
+
+  checkConnected(){
+    if(this.ready_local && this.ready_remote && !this.ready){
+      this.ready = true
+      this.closed = false
+      this.emit('connect', true)
+      debug('checkConnected - connected')
+      this.channel.post('connected', true)
+    }
+    else if(!this.ready){
+      debug('checkConnected - not ready')
+      this.channel.post('connected', true)
+    }
+  }
+
+  onconnected(){
+    debug('onconnected')
+    if(!this.ready_remote){
+      debug('make ready')
+      this.ready_remote = true
+      this.checkConnected()
+    }
+  }
+
+  onclose(){
+    debug('onclose')
+    if(this.ready && !this.closed){
+      this.ready = false
+      this.closed = true
+      this.ready_remote = false
+      this.ready_local = false
+
+      this.emit('close')
+    }
+  }
+
+  ondata(msg){
+    debug('ondata')
+    if(this.ready && !this.closed){
+      this.emit('data', msg)
+    }
+  }
+
+  send(msg){
+    debug('send')
+    if(this.ready && !this.closed){
+      this.channel.post('data', msg)
+    }
+  }
+
+  destroy(){
+    this.close()
+    delete this.channel
+  }
+
+  close(){
+    debug('close')
+    if(this.ready && !this.closed){
+      this.ready = false
+      this.ready_local = false
+      this.ready_remote = false
+      this.closed = true
+      this.channel.post('close', true)
+    }
+  }
+}
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_peer-comms.js.html b/docs/comms_peer-comms.js.html new file mode 100644 index 0000000..f6d867b --- /dev/null +++ b/docs/comms_peer-comms.js.html @@ -0,0 +1,383 @@ +Source: comms/peer-comms.js
On this page

comms_peer-comms.js

const debug = require('debug')('dataparty.comms.peercomms')
+const uuidv4 = require('uuid/v4')
+const HttpMocks = require('node-mocks-http')
+
+const SocketOp = require('./op/socket-op')
+const ISocketComms = require('./isocket-comms')
+
+const Joi = require('@hapi/joi')
+const HostOp = require('./host/host-op')
+const HostProtocolScheme = require('./host/host-protocol-scheme')
+
+const AUTH_TIMEOUT_MS = 3000
+
+const HOST_SESSION_STATES = {
+  AUTH_REQUIRED: 'AUTH_REQUIRED',
+  AUTHED: 'AUTHED',
+  SERVER_CLOSED: 'SERVER_CLOSED',
+  CLIENT_CLOSED: 'CLIENT_CLOSED'
+}
+
+
+function truncateString(str, num) {
+
+  if(!str){return ''}
+  
+  if(typeof str != 'string'){
+    str = str.toString()
+  }
+
+  let length = str.length
+
+  if (str.length <= num) {
+    return str
+  }
+  return str.slice(0, num) + '...' + (length-num) + 'more bytes'
+}
+
+
+/**
+ * @class module:Comms.PeerComms
+ * @implements {module:Comms.ISocketComms}
+ * @extends {module:Comms.ISocketComms}
+ * @link module:Comms
+ * 
+ */
+class PeerComms extends ISocketComms {
+  constructor({remoteIdentity, discoverRemoteIdentity, host, party, socket, ...options}){
+    super({remoteIdentity, discoverRemoteIdentity, party, ...options})
+
+    this.uuid = uuidv4()
+    this.socket = socket || null
+
+    this.host = host   //! Is comms host\
+    this.oncall = null
+
+    this._host_auth_timeout = null
+
+    if(this.host){
+      this.state = PeerComms.STATES.AUTH_REQUIRED
+      this.session = undefined
+      this.identity = undefined
+      this.actor = undefined
+    }
+
+    this.pending_calls = 0
+  }
+
+  setState(state) {
+    this.state = state
+    this.emit('state', this.state)
+  }
+
+  static get STATES() {
+    return HOST_SESSION_STATES
+  }
+
+  async handleClientCall(message){
+
+    debug('handleClientCall - pending calls - ', this.pending_calls)
+
+    this.pending_calls++
+    try{
+
+      let response = null
+      let request = await this.decrypt( {data: message}, this.remoteIdentity )
+      debug('handleHostCall', truncateString(request, 1024))
+
+      let inputValidated
+
+      if (this.state === PeerComms.STATES.AUTHED) {
+
+        if(typeof request != 'object'){
+          request = JSON.parse(request)
+        }
+
+        debug('handling authed call')
+        inputValidated = HostProtocolScheme.ANY_OP.validate(request)
+      } else if (this.state === PeerComms.STATES.AUTH_REQUIRED) {
+        debug('handling non-authed call')
+        inputValidated = HostProtocolScheme.AUTH_OP.validate(request)
+      } else {
+        throw new Error(
+          'Recieved input in unexpected session state [',
+          this.state,
+          ']'
+        )
+      }
+
+      if(inputValidated.error !== undefined){
+        throw inputValidated.error
+      }
+
+      //debug('original input ->', typeof request, request)
+      //debug('validated input ->', inputValidated)
+      const op = new HostOp({ msg: message, input: inputValidated.value })
+
+      /*debug('session id : ', op.input.session, this.session)
+
+      if (this.session && op.input.session === this.session.id) {
+        debug('session id MATCH')
+      }*/
+
+      op.once('finished', state => {
+        const response = {
+          op: 'status',
+          id: op.id,
+          level: op.level,
+          state: op.state,
+          stats: {
+            start: op.start,
+            end: op.end,
+            duration_ms: op.end - op.start
+          },
+          ...op.result 
+        }
+
+        debug('finished', response.id, response.state, response.stats.duration_ms, 'ms')
+
+        this.send(response)
+      })
+
+
+      
+      await this.authorizeOperation(op)
+
+    } catch (err) {
+      debug('EXCEPTION ->', err)
+    }
+    this.pending_calls--
+  }
+
+  
+
+  async handleClientConnection(){
+    debug('handleClientConnection')
+
+    this._host_auth_timeout = setTimeout(
+      this.handleAuthTimeout.bind(this),
+      AUTH_TIMEOUT_MS
+    )
+  }
+
+
+
+  async handleAuthTimeout(){
+    clearTimeout(this._host_auth_timeout)
+    this._host_auth_timeout = null
+    if(!this.authed){
+      debug('handleAuthTimeout - timed out')
+      this.authed = false
+      await this.stop()
+    }
+  }
+
+  async handleMessage(message){
+    debug('handleMessage', truncateString(message.toString(), 1024) )
+
+    this.onmessage({data: message})
+  }
+
+  async call(path, data, force=false){
+    if(this.host && !this.force){ throw new Error('host-not-allowed-call') }
+    if(!this.authed){ throw new Error('not authed') }
+
+    if (!this.party.hasIdentity()) {
+      throw new Error('identity required')
+    }
+
+    let callOp = new SocketOp( 'peer-call', { endpoint: path, data }, this )
+
+    debug('running peer-call endpoint =', path, truncateString(data, 1024))
+
+    const reply = await callOp.run()
+
+    return reply.result
+  }
+
+  async start(){
+    debug('start')
+    if(this.socketInit){
+      await this.socketInit()
+    }
+    
+    this.socket.on('close', this.stop.bind(this))
+
+    if(this.host){
+      debug('host mode comms')
+
+      this.socket.once('connect', this.handleClientConnection.bind(this))
+      this.socket.on('data', this.handleClientCall.bind(this))
+    }
+    else{
+      debug('client mode comms')
+      this.socket.once('connect', this.onopen.bind(this))
+      this.socket.on('data', this.handleMessage.bind(this))
+    }
+
+    if(this.socketStart){
+      await this.socketStart()
+    }
+  }
+
+  async stop(){
+    debug('stop')
+    this.close()
+  }
+
+  async close(){
+    debug('close', this.uuid)
+
+    if(this.party.topics){
+      await this.party.topics.destroyNode(this)
+    }
+
+    debug('closing connection')
+    this.socket.destroy()
+
+    this.onclose()
+  }
+
+
+  async authorizeOperation(op) {
+
+    //debug('Here\'s op', op)
+    
+    //debug('state : ', this.state)
+
+    //console.log(op.input)
+
+    if (op.op === 'auth' && this.state === PeerComms.STATES.AUTH_REQUIRED) {
+    
+      debug('handling auth op')
+      return this.handleAuthOp(op)
+    
+    } else if (op.op === 'peer-call' && this.state === PeerComms.STATES.AUTHED) {
+
+      return this.handleCallOp(op)
+
+    } else if (op.op === 'advertise' && this.state === PeerComms.STATES.AUTHED) {
+
+      if(this.party.topics){
+        await this.party.topics.advertise(this, op.input.topic)
+        op.setState(HostOp.STATES.Finished_Success)
+      }
+      else{
+        op.setState(HostOp.STATES.Finished_Fail)
+      }
+
+    } else if (op.op === 'subscribe' && this.state === PeerComms.STATES.AUTHED) {
+
+      if(this.party.topics){
+        await  this.party.topics.subscribe.bind(this.party.topics)(this, op.input.topic)
+        op.setState(HostOp.STATES.Finished_Success)
+      }
+      else{
+        op.setState(HostOp.STATES.Finished_Fail)
+      }
+
+    } else if (op.op === 'unsubscribe' && this.state === PeerComms.STATES.AUTHED) {
+
+      if(this.party.topics){
+        await this.party.topics.unsubscribe(this, op.input.topic)
+        op.setState(HostOp.STATES.Finished_Success)
+      }
+      else{
+        op.setState(HostOp.STATES.Finished_Fail)
+      }
+
+    } else if (op.op === 'publish' && this.state === PeerComms.STATES.AUTHED) {
+
+      if(this.party.topics){
+        await this.party.topics.publish(this, op.input.topic, op.input.msg)
+        op.setState(HostOp.STATES.Finished_Success)
+      }
+      else{
+        op.setState(HostOp.STATES.Finished_Fail)
+      }
+
+    } else {
+      debug('⚠️ op not implemented ⚠️')
+      debug(op.input)
+
+      op.result='not implemented'
+      op.setState(HostOp.STATES.Finished_Fail)
+    }
+  }
+
+  async handleAuthOp(op){
+    
+    debug('allowing client - ', this.remoteIdentity)
+
+    clearTimeout(this._host_auth_timeout)
+    this._host_auth_timeout = null
+
+    this.authed = true
+    this.setState(PeerComms.STATES.AUTHED)
+    op.setState(HostOp.STATES.Finished_Success)
+
+    this.emit('open')
+    return 
+  }
+
+  async handleCallOp(op){
+    debug('peer-call', op.input.endpoint)
+
+    if(this.party.hostRunner){
+
+      debug('calling runner')
+
+      if(op.input.endpoint == 'api-v2-peer-bouncer'){
+        debug('ask->', truncateString(op.input.data, 1024))
+        op.result = {result: await this.party.handleCall(op.input.data) }
+
+        op.setState(HostOp.STATES.Finished_Success)
+        return
+      }
+
+      const req = HttpMocks.createRequest({
+        method: 'GET',
+        url: '/'+op.input.endpoint,
+        body: (op.input.data) ? JSON.parse(op.msg.toString()) : undefined
+      })
+
+      const res = HttpMocks.createResponse()
+
+      debug('\tthe request', req)
+
+      debug('req ip type', typeof req.ip)
+
+      const route = this.party.hostRunner.router.get(op.input.endpoint)
+
+      debug('route',route)
+
+      debug('call route', await route._events.route({
+        method: req.method,
+        pathname: req.url,
+        request: req,
+        response: res
+      }))
+
+      op.result = {result: res._getData() }
+
+      debug('got result', op.result)
+
+      op.setState(HostOp.STATES.Finished_Success)
+      return
+
+    } else if(op.input.endpoint == 'api-v2-peer-bouncer'){
+      
+      debug('ask->',op.input.data)
+      op.result = {result: await this.party.handleCall(op.input.data) }
+
+      op.setState(HostOp.STATES.Finished_Success)
+
+      return
+    }
+  }
+}
+
+
+module.exports = PeerComms
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_rest-comms.js.html b/docs/comms_rest-comms.js.html new file mode 100644 index 0000000..e3c619d --- /dev/null +++ b/docs/comms_rest-comms.js.html @@ -0,0 +1,386 @@ +Source: comms/rest-comms.js
On this page

comms_rest-comms.js

const axios = require('axios')
+const EventEmitter = require('eventemitter3')
+const debug = require('debug')('dataparty.comms.rest')
+
+const dataparty_crypto = require('@dataparty/crypto')
+
+//const WebsocketComms = require('./old-websocket-comms')
+const AuthError = require('../errors/auth-error')
+
+
+const DEFAULT_REST_TIMEOUT = 30000
+
+/**
+ * @class module:Comms.RestComms
+ * @link module:Comms
+ * @extends EventEmitter
+ */
+class RestComms extends EventEmitter {
+  constructor({ remoteIdentity, config, party }) {
+    super()
+    this.uri = undefined
+    this.wsUri = undefined
+    this.cfgPrefix = 'rest'
+    this.uriPrefix = ''
+    this.config = config
+    this.sessionId = undefined
+    this.remoteIdentity = remoteIdentity
+    this.websocketComm = undefined
+    this.party = party
+
+    this.authed = undefined
+
+    // debug(this.uri)
+  }
+
+  hasSession() {
+    return !!this.sessionId
+  }
+
+  async stop() {
+    if (this.websocketComm && this.websocketComm.connected) {
+      debug('cleaning up websocket')
+
+      this.websocketComm.close()
+    }
+  }
+
+  async start() {
+    await this.loadCloud()
+    await this.party.loadIdentity()
+    await this.party.loadActor()
+    await this.loadSession()
+
+    if (this.authed) {
+      return
+    }
+
+    if (this.party.hasActor() && this.party.hasIdentity()) {
+      if (this.hasSession()) {
+        debug('RECOVERING SESSION')
+        return this.authRecover().catch(this.allocateSession.bind(this))
+      }
+
+      debug('ALLOCATING SESSION')
+      return this.allocateSession()
+    }
+
+    throw new Error('client needs to be enrolled')
+  }
+
+  async loadSession() {
+    const path = this.cfgPrefix + '.rest-session'
+    const localSessionObj = this.config.read(path)
+
+    if (!localSessionObj) {
+      return
+    }
+
+    this.sessionId = localSessionObj.id
+    await this.storeSession()
+
+    debug('loaded rest session', this.sessionId)
+  }
+
+  async loadCloud() {
+    this.uri = this.config.read('cloud.uri')
+    this.wsUri = this.config.read('cloud.wsUri')
+
+    if (this.uri && this.uri[this.uri.length - 1] !== '/') {
+      this.uri = this.uri + '/'
+    }
+  }
+
+  clearSession() {
+    //
+  }
+
+  storeSession() {
+    const path = this.cfgPrefix + '.rest-session'
+    this.config.write(path, { id: this.sessionId })
+  }
+
+  async call(path, data, 
+    {
+      expectClearTextReply = false,
+      sendClearTextRequest = false,
+      useSessions = true
+    } = {}
+  ) {
+    if (!this.uri) {
+      await this.loadCloud()
+    }
+    if (!this.party.hasIdentity()) {
+      throw new Error('identity required')
+    }
+    await this.getServiceIdentity()
+
+    //const obj = { session: this.sessionId, data: data }
+
+    const fullPath = this.uri + this.uriPrefix + path
+    
+
+    let content = null
+
+    if(data || this.useSessions){
+      content = {data}
+      
+      if(useSessions){ content.session = this.sessionId }
+
+      if(!sendClearTextRequest){
+        content = await this.party.encrypt(content, this.remoteIdentity)
+      }
+    }
+
+    debug('call', fullPath, ' req - ', JSON.stringify(content))
+
+    let reply
+    try {
+      reply = await RestComms.HttpPost(fullPath, content)
+      //reply = JSON.parse(str)
+
+      // debug('raw reply ->', reply)
+    } catch (error) {
+      debug('rest', fullPath, ' call fail ->', error.message)
+      throw new Error('RestCommsError')
+    }
+
+    const msg = await this.party.decrypt(
+      reply,
+      this.remoteIdentity,
+      expectClearTextReply
+    )
+
+    debug('call', fullPath, ' res - ', JSON.stringify(msg, null, 2))
+
+    return msg
+  }
+
+  async syncActors() {
+    const info = await this.call('actor-info')
+
+    debug('syncActors - got info', JSON.stringify(info, null, 2))
+
+    return this.populateActors(info.actor.actors)
+  }
+
+  async populateActors(actorRefs) {
+    const actorLookups = []
+    for (const actorInfo of actorRefs) {
+      debug('looking up actor', actorInfo)
+
+      const lookup = this.party
+        .find()
+        .type(actorInfo.type)
+        .id(actorInfo.id)
+        .exec()
+        .then(docs => {
+          if (docs.length === 1) {
+            debug('found actor', docs[0])
+            return docs[0]
+          } else {
+            debug('failed to read actor', actorInfo, docs)
+          }
+
+          return undefined
+        })
+
+      actorLookups.push(lookup)
+      // await lookup
+      debug('found actor', actorInfo, lookup)
+    }
+
+    // return this
+
+    return Promise.all(actorLookups).then(docs => {
+      this.party.actors = docs
+
+      return this
+    })
+  }
+
+  async getServiceIdentity() {
+    if (!this.remoteIdentity) {
+      if (!this.uri) {
+        await this.loadCloud()
+      }
+      const serverIdentity = await RestComms.HttpGet(this.uri + `${this.uriPrefix}identity`)
+      debug('server identity - ', serverIdentity)
+
+      this.remoteIdentity = new dataparty_crypto.Identity(serverIdentity)
+    }
+
+    return this.remoteIdentity
+  }
+
+  async authorized() {
+    await new Promise((resolve, reject) => {
+      if (this.authed) {
+        return resolve()
+      }
+      this.once('open', resolve)
+      this.once('close', reject)
+    })
+    return this
+  }
+
+  async redeemInvite(code) {
+    // await this.party.loadIdentity()
+    return this.call('claim-user-invite', {
+      short_code: code
+    })
+  }
+
+  authGitHub(code) {
+    // call server endpoint for github oauth
+    // store returned session
+    if (!this.uri) {
+      this.loadCloud()
+    }
+
+    return this.party
+      .loadIdentity()
+      .then(() => {
+        return this.call('oauth-github', { code: code })
+      })
+      .then(sessionInfo => {
+        debug(sessionInfo)
+
+        this.sessionId = sessionInfo.session
+        this.authed = true
+
+        this.storeSession()
+        this.emit('open')
+
+        return this.populateActors(sessionInfo.actor.actors.slice(0, 2)).then(
+          () => {
+            this.storeSession()
+            this.emit('open')
+          }
+        )
+      })
+  }
+
+  async authRecover() {
+    debug('AUTH RECOVER')
+    await this.party.loadActor()
+    await this.loadSession()
+
+    if (
+      !this.party.actor ||
+      !this.party.actor.id ||
+      !this.party.actor.type ||
+      !this.sessionId
+    ) {
+      this.authed = false
+      debug('session data missing, cannot recover session')
+      this.emit('close')
+      throw new Error('session data missing')
+    }
+
+    debug('syncing actors')
+
+    try {
+      await this.syncActors()
+      this.authed = true
+      this.emit('open')
+    } catch (err) {
+      debug('auth error', err)
+      this.authed = false
+      this.emit('close')
+
+      throw new AuthError('auth error')
+    }
+  }
+
+  async allocateSession() {
+    debug('ALLOCATE SESSION')
+    this.party.loadActor()
+
+    if (!this.party.actor || !this.party.actor.id || !this.party.actor.type) {
+      this.authed = false
+      this.emit('close')
+      debug('actor data missing, cannot allocate session')
+      throw new Error('actor data missing, cannot allocate session')
+    }
+
+    debug('actor', this.party.actor)
+
+    try {
+      const reply = await this.call('rest-session', {
+        actor: {
+          id: this.party.actor.id,
+          type: this.party.actor.type
+        }
+      })
+
+      this.sessionId = reply.session
+      this.authed = true
+      this.storeSession()
+
+      await this.syncActors()
+      this.emit('open')
+    } catch (err) {
+      debug('auth error', err)
+      this.authed = false
+      this.emit('close')
+
+      throw new AuthError('auth error')
+    }
+  }
+
+  /*
+  async websocket(reuse = true) {
+    if (reuse && this.websocketComm && this.websocketComm.connected) {
+      return this.websocketComm
+    }
+
+    return this.call('websocket-session').then(reply => {
+      debug(reply)
+      if (!this.wsUri) {
+        this.loadCloud()
+      }
+
+      const comm = new WebsocketComms({
+        uri: this.wsUri,
+        session: reply.websocket_session,
+        identity: this.party._identity,
+        remoteIdentity: this.remoteIdentity
+      })
+
+      if (reuse) {
+        this.websocketComm = comm
+      }
+
+      return comm.authorized()
+    })
+  }*/
+
+  static async HttpRequest(verb, url, data) {
+
+    debug(`${verb} - ${url}`)
+
+    const response = await axios({
+      method: verb,
+      url,
+      data,
+      headers: {'Content-Type': 'application/json'},
+      timeout: DEFAULT_REST_TIMEOUT
+    })
+
+    return response.data
+  }
+
+  static async HttpGet(url) {
+    return RestComms.HttpRequest('GET', url)
+  }
+
+  static async HttpPost(url, body) {
+    return RestComms.HttpRequest('POST', url, body)
+  }
+}
+
+module.exports = RestComms
+
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_rtc-socket-comms.js.html b/docs/comms_rtc-socket-comms.js.html new file mode 100644 index 0000000..348917a --- /dev/null +++ b/docs/comms_rtc-socket-comms.js.html @@ -0,0 +1,34 @@ +Source: comms/rtc-socket-comms.js
On this page

comms_rtc-socket-comms.js

const debug = require('debug')('dataparty.comms.rtcsocketcomms')
+
+const SimplePeer = require('simple-peer')
+const PeerComms = require('./peer-comms')
+
+/**
+ * @class module:Comms.RTCSocketComms
+ * @implements {module:Comms.ISocketComms}
+ * @extends {module:Comms.PeerComms}
+ * @link module:Comms
+ * @see https://en.wikipedia.org/wiki/WebRTC
+ */
+class RTCSocketComms extends PeerComms {
+  constructor({remoteIdentity, host, party, wrtc, trickle = false, ...options}){
+    super({remoteIdentity, host, party, ...options})
+
+    this.rtcSettings = {
+      wrtc,
+      trickle,
+      initiator: host
+    }
+  }
+
+
+  async socketInit(){
+    debug('init')
+    this.socket = new SimplePeer(this.rtcSettings)
+  }
+}
+
+
+module.exports = RTCSocketComms
@dataparty/api
\ No newline at end of file diff --git a/docs/comms_websocket-comms.js.html b/docs/comms_websocket-comms.js.html new file mode 100644 index 0000000..469fc05 --- /dev/null +++ b/docs/comms_websocket-comms.js.html @@ -0,0 +1,48 @@ +Source: comms/websocket-comms.js
On this page

comms_websocket-comms.js

const debug = require('debug')('dataparty.comms.websocket')
+
+const WebSocket = global.WebSocket ? global.WebSocket : require('ws')
+
+const PeerComms = require('./peer-comms')
+
+const WebsocketShim = require('./websocket-shim')
+
+/**
+ * @class module:Comms.WebsocketComms
+ * @implements {module:Comms.ISocketComms}
+ * @extends {module:Comms.PeerComms}
+ * @link module:Comms
+ * @see https://en.wikipedia.org/wiki/WebSocket
+ */
+class WebsocketComms extends PeerComms {
+  constructor({uri, connection, remoteIdentity, host, party, ...options}){
+    super({remoteIdentity, host, party, ...options})
+
+    this.uri = uri
+    this.connection = connection
+
+    if(this.host && !this.connection){
+      throw new Error('existing connection expected')
+    }
+
+    if(!this.host && (!this.uri && !this.connection)){
+      throw new Error('uri or existing connection expected')
+    }
+  }
+
+
+  async socketInit(){
+    debug('init')
+    
+    if(!this.host && !this.connection){
+      debug('opening client connection to',this.uri)
+      this.connection = new WebSocket(this.uri)
+    }
+
+    this.socket = new WebsocketShim(this.connection)
+  }
+}
+
+
+module.exports = WebsocketComms
@dataparty/api
\ No newline at end of file diff --git a/docs/config_index.js.html b/docs/config_index.js.html new file mode 100644 index 0000000..c1ecc95 --- /dev/null +++ b/docs/config_index.js.html @@ -0,0 +1,12 @@ +Source: config/index.js
On this page

config_index.js


+
+/**
+* @module Config
+*/
+
+exports.NconfConfig = require('./nconf')
+exports.MemoryConfig = require('./memory')
+exports.JsonFileConfig = require('./json-file')
+exports.LocalStorageConfig = require('./local-storage')
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.BLEPeerClient.html b/docs/module-Comms.BLEPeerClient.html new file mode 100644 index 0000000..866f61e --- /dev/null +++ b/docs/module-Comms.BLEPeerClient.html @@ -0,0 +1,3 @@ +Class: BLEPeerClient
On this page

Comms. BLEPeerClient

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.I2pSocketComms.html b/docs/module-Comms.I2pSocketComms.html new file mode 100644 index 0000000..15e4b94 --- /dev/null +++ b/docs/module-Comms.I2pSocketComms.html @@ -0,0 +1,3 @@ +Class: I2pSocketComms
On this page

Comms. I2pSocketComms

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.ISocketComms.html b/docs/module-Comms.ISocketComms.html new file mode 100644 index 0000000..750138a --- /dev/null +++ b/docs/module-Comms.ISocketComms.html @@ -0,0 +1,3 @@ +Interface: ISocketComms
On this page

Comms. ISocketComms

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.LoopbackChannel.html b/docs/module-Comms.LoopbackChannel.html new file mode 100644 index 0000000..3a603d5 --- /dev/null +++ b/docs/module-Comms.LoopbackChannel.html @@ -0,0 +1,3 @@ +Class: LoopbackChannel
On this page

Comms. LoopbackChannel

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.LoopbackComms.html b/docs/module-Comms.LoopbackComms.html new file mode 100644 index 0000000..c0ee95f --- /dev/null +++ b/docs/module-Comms.LoopbackComms.html @@ -0,0 +1,3 @@ +Class: LoopbackComms
On this page

Comms. LoopbackComms

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.LoopbackSocket.html b/docs/module-Comms.LoopbackSocket.html new file mode 100644 index 0000000..f6ed239 --- /dev/null +++ b/docs/module-Comms.LoopbackSocket.html @@ -0,0 +1,3 @@ +Class: LoopbackSocket
On this page

Comms. LoopbackSocket

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.PeerComms.html b/docs/module-Comms.PeerComms.html new file mode 100644 index 0000000..b088cb7 --- /dev/null +++ b/docs/module-Comms.PeerComms.html @@ -0,0 +1,3 @@ +Class: PeerComms
On this page

Comms. PeerComms

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.RTCSocketComms.html b/docs/module-Comms.RTCSocketComms.html new file mode 100644 index 0000000..2b866f8 --- /dev/null +++ b/docs/module-Comms.RTCSocketComms.html @@ -0,0 +1,3 @@ +Class: RTCSocketComms
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.RestComms.html b/docs/module-Comms.RestComms.html new file mode 100644 index 0000000..17bae19 --- /dev/null +++ b/docs/module-Comms.RestComms.html @@ -0,0 +1,3 @@ +Class: RestComms
On this page

Comms. RestComms

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.WebsocketComms.html b/docs/module-Comms.WebsocketComms.html new file mode 100644 index 0000000..f328014 --- /dev/null +++ b/docs/module-Comms.WebsocketComms.html @@ -0,0 +1,3 @@ +Class: WebsocketComms
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Comms.html b/docs/module-Comms.html new file mode 100644 index 0000000..9ef66c7 --- /dev/null +++ b/docs/module-Comms.html @@ -0,0 +1,3 @@ +Module: Comms
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.IConfig.html b/docs/module-Config.IConfig.html new file mode 100644 index 0000000..a1c6075 --- /dev/null +++ b/docs/module-Config.IConfig.html @@ -0,0 +1,3 @@ +Interface: IConfig
On this page

Config. IConfig

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.JsonFileConfig.html b/docs/module-Config.JsonFileConfig.html new file mode 100644 index 0000000..438739f --- /dev/null +++ b/docs/module-Config.JsonFileConfig.html @@ -0,0 +1,3 @@ +Class: JsonFileConfig
On this page

Config. JsonFileConfig

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.LocalStorageConfig.html b/docs/module-Config.LocalStorageConfig.html new file mode 100644 index 0000000..3273495 --- /dev/null +++ b/docs/module-Config.LocalStorageConfig.html @@ -0,0 +1,3 @@ +Class: LocalStorageConfig
On this page

Config. LocalStorageConfig

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.MemoryConfig.html b/docs/module-Config.MemoryConfig.html new file mode 100644 index 0000000..34281ee --- /dev/null +++ b/docs/module-Config.MemoryConfig.html @@ -0,0 +1,3 @@ +Class: MemoryConfig
On this page

Config. MemoryConfig

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.NconfConfig.html b/docs/module-Config.NconfConfig.html new file mode 100644 index 0000000..a911c39 --- /dev/null +++ b/docs/module-Config.NconfConfig.html @@ -0,0 +1,3 @@ +Class: NconfConfig
On this page

Config. NconfConfig

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Config.html b/docs/module-Config.html new file mode 100644 index 0000000..980613b --- /dev/null +++ b/docs/module-Config.html @@ -0,0 +1,3 @@ +Module: Config
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Db.IDb.html b/docs/module-Db.IDb.html new file mode 100644 index 0000000..96e923d --- /dev/null +++ b/docs/module-Db.IDb.html @@ -0,0 +1,3 @@ +Interface: IDb
On this page

Db. IDb

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Db.LokiDb.html b/docs/module-Db.LokiDb.html new file mode 100644 index 0000000..5a702bc --- /dev/null +++ b/docs/module-Db.LokiDb.html @@ -0,0 +1,3 @@ +Class: LokiDb
On this page

Db. LokiDb

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Db.TingoDb.html b/docs/module-Db.TingoDb.html new file mode 100644 index 0000000..d8c89b4 --- /dev/null +++ b/docs/module-Db.TingoDb.html @@ -0,0 +1,3 @@ +Class: TingoDb
On this page

Db. TingoDb

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Db.ZangoDb.html b/docs/module-Db.ZangoDb.html new file mode 100644 index 0000000..6fa2967 --- /dev/null +++ b/docs/module-Db.ZangoDb.html @@ -0,0 +1,3 @@ +Class: ZangoDb
On this page

Db. ZangoDb

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Db.html b/docs/module-Db.html new file mode 100644 index 0000000..1285b9a --- /dev/null +++ b/docs/module-Db.html @@ -0,0 +1,3 @@ +Module: Db
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.CloudParty.html b/docs/module-Party.CloudParty.html new file mode 100644 index 0000000..8419cad --- /dev/null +++ b/docs/module-Party.CloudParty.html @@ -0,0 +1,3 @@ +Class: CloudParty
On this page

Party. CloudParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.IDocument.html b/docs/module-Party.IDocument.html new file mode 100644 index 0000000..11eb0b0 --- /dev/null +++ b/docs/module-Party.IDocument.html @@ -0,0 +1,3 @@ +Interface: IDocument
On this page

Party. IDocument

Represents a document with caching and local+remote change notifications

Extends

  • EventEmitter
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.IParty.html b/docs/module-Party.IParty.html new file mode 100644 index 0000000..88878b2 --- /dev/null +++ b/docs/module-Party.IParty.html @@ -0,0 +1,3 @@ +Interface: IParty
On this page

Party. IParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.LokiCache.html b/docs/module-Party.LokiCache.html new file mode 100644 index 0000000..480dd06 --- /dev/null +++ b/docs/module-Party.LokiCache.html @@ -0,0 +1,3 @@ +Class: LokiCache
On this page

Party. LokiCache

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.LokiParty.html b/docs/module-Party.LokiParty.html new file mode 100644 index 0000000..78d35f0 --- /dev/null +++ b/docs/module-Party.LokiParty.html @@ -0,0 +1,3 @@ +Class: LokiParty
On this page

Party. LokiParty

new LokiParty(path, dbAdapter, lokiOptions)

A local party based on LokiJS.

Parameters:
NameTypeDescription
pathstring

Path on filesystem to lokijs db file

dbAdapterLokiAdapater

Lokijs db adapter, see: http://techfort.github.io/LokiJS/tutorial-Persistence%20Adapters.html

lokiOptionsObject

Options to pass to lokijs see: http://techfort.github.io/LokiJS/Loki.html

Extends

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.MongoParty.html b/docs/module-Party.MongoParty.html new file mode 100644 index 0000000..df02ede --- /dev/null +++ b/docs/module-Party.MongoParty.html @@ -0,0 +1,3 @@ +Class: MongoParty
On this page

Party. MongoParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.PeerParty.html b/docs/module-Party.PeerParty.html new file mode 100644 index 0000000..3bf4f74 --- /dev/null +++ b/docs/module-Party.PeerParty.html @@ -0,0 +1,3 @@ +Class: PeerParty
On this page

Party. PeerParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.Query.html b/docs/module-Party.Query.html new file mode 100644 index 0000000..3f51548 --- /dev/null +++ b/docs/module-Party.Query.html @@ -0,0 +1,3 @@ +Class: Query
On this page

Party. Query

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.TingoParty.html b/docs/module-Party.TingoParty.html new file mode 100644 index 0000000..8d9a3cf --- /dev/null +++ b/docs/module-Party.TingoParty.html @@ -0,0 +1,3 @@ +Class: TingoParty
On this page

Party. TingoParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.ZangoParty.html b/docs/module-Party.ZangoParty.html new file mode 100644 index 0000000..d93f495 --- /dev/null +++ b/docs/module-Party.ZangoParty.html @@ -0,0 +1,3 @@ +Class: ZangoParty
On this page

Party. ZangoParty

@dataparty/api
\ No newline at end of file diff --git a/docs/module-Party.html b/docs/module-Party.html new file mode 100644 index 0000000..189816f --- /dev/null +++ b/docs/module-Party.html @@ -0,0 +1,3 @@ +Module: Party
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/module-Service.html b/docs/module-Service.html new file mode 100644 index 0000000..98af93b --- /dev/null +++ b/docs/module-Service.html @@ -0,0 +1,3 @@ +Module: Service
On this page
@dataparty/api
\ No newline at end of file diff --git a/docs/party_index.js.html b/docs/party_index.js.html new file mode 100644 index 0000000..be67385 --- /dev/null +++ b/docs/party_index.js.html @@ -0,0 +1,14 @@ +Source: party/index.js
On this page

party_index.js

/** @module Party */
+exports.IParty = require('./iparty')
+exports.PeerParty = require('./peer/peer-party')
+exports.CloudParty = require('./cloud/cloud-party')
+exports.LokiParty = require('./local/loki-party')
+exports.TingoParty = require('./local/tingo-party')
+exports.ZangoParty = require('./local/zango-party')
+exports.MongoParty = require('./mongo/mongo-party')
+exports.IDocument = require('./idocument')
+exports.DocumentFactory = require('./document-factory')
+exports.CloudDocument = require('./cloud/cloud-document')
+
@dataparty/api
\ No newline at end of file diff --git a/docs/party_loki-cache.js.html b/docs/party_loki-cache.js.html new file mode 100644 index 0000000..998eb21 --- /dev/null +++ b/docs/party_loki-cache.js.html @@ -0,0 +1,195 @@ +Source: party/loki-cache.js
On this page

party_loki-cache.js

'use strict'
+
+const cloneDeep = require('lodash/cloneDeep')
+const Loki = require('lokijs')
+const EventEmitter = require('eventemitter3')
+const debug = require('debug')('dataparty.loki-cache')
+
+/**
+ * @class module:Party.LokiCache
+ * @link module.Party
+ */
+module.exports = class LokiCache extends EventEmitter {
+
+  constructor () {
+    super()
+    this.db = new Loki('app.dataparty.io/cache')
+  }
+
+  async start(){
+    return Promise.resolve()
+  }
+
+  _emitChange(msg, change){
+    const { type, id, revision } = msg.$meta
+    this.emit(
+      `${type}:${id}`,
+      {
+        event: change,
+        msg: { type, id, revision }
+      }
+    )
+  }
+
+  remove(type, id){
+    debug('remove', type, id)
+    var collection = this.db.getCollection(type)
+
+    collection.chain().find({
+      '$meta.id': id
+    }).remove()
+
+    var found = collection.findOne({'$meta.id': id})
+
+    debug(found)
+    if(found){ 
+    
+      try{
+        collection.remove(found)  
+      }
+      catch(exception){
+        debug('remove CATCH -', exception)
+        collection.findAndRemove({'$meta.id': id})
+      }
+    }
+
+    var item = this.findById(type, id)
+
+    debug(item)
+
+    if(!item){
+      debug('remove, TEST - no item found')
+    }
+    else{
+      debug('remove, TEST - found item')
+      //throw 'remove failed'
+    }
+  }
+
+  findById(type, id){
+    const cachedMsg = this.db.getCollection(type).findOne({ '$meta.id': id })
+
+    if(cachedMsg){
+      delete cachedMsg.$loki
+      delete cachedMsg.meta
+    }
+
+    return cachedMsg
+  }
+
+  // insert list of msgs (& msg invalidations) into cache
+  // * messages are inserted into collection indicated by required _type field
+  // * requires unique msg.$meta.id field for each msg
+  // * if inserted msg.$meta.error or msg.$meta.removed is truthy delete
+  insert (msgs) {
+    return new Promise((resolve, reject) => {
+
+      for (const msg of msgs) {
+        debug('inserting msg ->', msg)
+
+        const { type, id, error, removed } = msg.$meta
+
+        // if collection for msg type isnt in cache, add it
+        if ( !this.db.getCollection(type) ) {
+
+          // create new table & index on unique $meta.id
+          // TODO -> index { unique: ['$meta.id'] }
+          this.db.addCollection(type)
+        }
+        const collection = this.db.getCollection(type)
+
+        // check for cached version of message
+        const cachedMsg = collection.findOne({ '$meta.id': id })
+
+        // if backend set error or removed flag invalidate cache
+        if (error || removed) {
+          debug('invalidating msg!')
+
+          if (cachedMsg) {
+            try{
+              //collection.remove(cachedMsg)
+              collection.findAndRemove({
+                '$meta.id': id,
+              })
+            }
+            catch(err){
+              debug('WARN', err)
+            }
+            this._emitChange(msg, 'remove')
+          }
+
+        // otherwise insert new message (remove old message if it exists)
+        } else {
+
+          debug('inserting msg', msg)
+
+          // check if msg is already in cache
+          if (cachedMsg) {
+            collection.findAndRemove({
+              '$meta.id': id,
+            })
+          }
+
+          // clone msg on insert - cache should follow backend
+          collection.insert(cloneDeep(msg))
+          
+
+          if(cachedMsg){
+            this._emitChange(msg, 'update')
+          }
+          else {
+            this._emitChange(msg, 'create')
+          }
+        }
+      }
+      resolve(true)
+    })
+  }
+
+  // takes list of metadata msgs to populate with params
+  // * reads metadata -> msg.$meta.type & msg.$meta.id
+  // * resolves to -> { hits: [populated msgs], misses: [original msgs] }
+  populate (msgs) {
+    return new Promise((resolve, reject) => {
+      debug('populating msgs ->', msgs)
+
+      const hits = []
+      const misses = []
+      for (const msg of msgs) {
+        const { type, id, revision } = msg.$meta || {}
+
+        const collection = this.db.getCollection(type)
+        if (collection) {
+
+          // get msg by id & strip loki metadata
+          const cachedMsg = Object.assign(
+            {},
+            collection.findOne({ '$meta.id': id})
+          )
+          delete cachedMsg.$loki
+          delete cachedMsg.meta
+          if (cachedMsg && cachedMsg.$meta && cachedMsg.$meta.id) {
+
+            if(revision > -1 && cachedMsg.$meta.revision != revision){
+              misses.push(msg)
+            }
+            else{
+              hits.push(cachedMsg)
+            }
+          } else {
+            misses.push(msg)
+          }
+        } else {
+          misses.push(msg)
+        }
+      }
+
+      debug('hits & misses ->', { hits, misses })
+
+      resolve({ hits, misses })
+    })
+  }
+}
+
@dataparty/api
\ No newline at end of file diff --git a/docs/party_query.js.html b/docs/party_query.js.html new file mode 100644 index 0000000..3b3263f --- /dev/null +++ b/docs/party_query.js.html @@ -0,0 +1,369 @@ +Source: party/query.js
On this page

party_query.js

'use strict'
+
+const debug=require('debug')('dataparty.query')
+const cloneDeep = require('lodash/cloneDeep')
+const Clerk = require('./clerk.js')
+
+// query builds query.spec object thru chained match links
+//
+// query.spec {
+//
+//   // header -> each field can optionally appear once at top level
+//   type: 'type' | types: ['type0' .. 'typeN'],
+//   id: 'xxx' | ids: ['xxx', 'yyy', 'zzz'],
+//   owner: { type: 'typeQ', id: 'qqq' },
+//   sort: { param: ['param', 'path'], direction: < -1 | 1 > },
+//   limit: count,
+//   select: [['filter'], ['on'], ['param', 'paths']],
+//
+//   // match operation tree
+//   // * ignored if 'id' or 'ids' fields are set
+//   // * generated from query chain
+//   // * param paths for match ops set from nearest preceding .where()
+//   // * executed on table(s) set by type(s) field otherwise all tables
+//   // * ops: and | or | equals | exists | in | gt | lt | size | all | elem
+//   match: [ // match implicitly ands list of match operations
+//     { op: 'equals', param: ['param', 'path'], value: 'value' },
+//     { op: 'or',
+//       match: [
+//         { op: 'in', param: ['param', 'path'], values: [x, y, z] },
+//         { op: 'and',
+//           match: [
+//             { op: 'gt', param: ['param', 'path'], value: min },
+//             { op: 'lt', param: ['param', 'path'], value: max },
+//           ],
+//         },
+//       ]
+//     },
+//     { op: 'size', param: ['path', 'to', 'list'], value: count },
+//     { op: 'elem',
+//       param: ['path', 'to', 'list'],
+//       match: [
+//         { op: 'in', param: ['color'], values: ['color0' .. 'colorN'] },
+//         { op: 'exists', param: ['name'], value: boolean },
+//       ],
+//     },
+//     { op: 'all', param: ['other', 'list'], values: [p, q, r] },
+//   ],
+// }
+
+/**
+ * @class  module:Party.Query
+ * @link module.Party
+ */
+module.exports = class Query {
+
+  constructor (qb, model) {
+    this.qb = qb
+    this.model = model
+
+    // starts with empty match tree
+    this.spec = { match: [] }
+
+    // variables to track the context of the match chain
+    this.currentWhere = undefined
+    this.whereStack = []
+    this.andOrElemStack = []
+    this.currentMatch = this.spec.match
+    this.matchStack = []
+  }
+
+  toJSON(){
+    return this.spec
+  }
+
+  // return a promise resolving to result of query
+  async exec (hydrate = true) {
+
+    if(!(typeof this.spec.type === 'string' && this.spec.type.length > 0)){
+      console.error(this.spec)
+      throw new Error ('Bad query')
+    }
+
+    if(hydrate){
+      const results = await this.qb.find(this.spec)
+      debug('hydrating', results)
+      return this.model.hydrate(results)
+    }
+    
+    return await this.qb.find(this.spec)
+  }
+
+  // *** match chain headers ***
+  //   -> not sensitive to position in chain
+  //   -> last call in chain overwrites earlier calls
+
+  // restrict query to msgs of given type
+  type (type) {
+    delete this.spec.types // mutually exclusive
+    this.spec.type = type
+    return this // enable chaining
+  }
+
+  // restrict query to msgs of given types
+  // *not compatible with type*
+  types (...types) {
+    delete this.spec.type // mutually exclusive
+    this.spec.types = types.slice() // copy array to avoid side effects
+    return this // enable chaining
+  }
+
+  // query for single msg by given id
+  // prereq -> type (*not* types)
+  // *all other query ops (except type) will be ignored*
+  id (id) {
+    delete this.spec.ids // mutually exclusive
+    this.spec.id = id
+    return this // enable chaining
+  }
+
+  // query for a list of msgs by given ids
+  // prereq -> type (*not* types)
+  // *all other query ops (except type) will be ignored*
+  ids (...ids) {
+    delete this.spec.id // mutually exclusive
+    this.spec.ids = ids.slice() // copy array to avoid side effects
+    return this // enable chaining
+  }
+
+  // restrict query to msgs with owner matching given type, id pair
+  owner (type, id) {
+    this.spec.owner = { type, id }
+    return this // enable chaining
+  }
+
+  // sort returned msgs on given param path (leading '-' reverses sort)
+  sort (param, direction) {
+    let cleanDirection = direction || 1
+    let cleanParam = param
+    if (cleanParam[0] === '-') {
+      cleanDirection = -1
+      cleanParam = cleanParam.slice(1) // remove leading '-'
+    }
+    this.spec.sort = { param: cleanParam, direction: cleanDirection }
+    return this // enable chaining
+  }
+
+  // limit # of msgs returned by query to a maximum of count
+  limit (count) {
+    this.spec.limit = count
+    return this // enable chaining
+  }
+
+  // filter fields from parameters of returned msgs
+  select (filter) {
+    this.spec.select = Clerk.splitFilter(filter)
+    return this // enable chaining
+  }
+
+  // *** match tree nodes ***
+
+  // sets context for following operations to given param path
+  where (param) {
+    this.currentWhere = Query.splitParam(param)
+    return this // enable chaining
+  }
+
+  // following path segments will be anded (default behavior)
+  and () {
+    const op = { op: 'and', match: [] }
+    this.currentMatch.push(op)
+
+    // push 'and' onto and or elem stack
+    this.andOrElemStack.push('and')
+
+    // push old match list onto match stack & set new ops match as current
+    this.matchStack.push(this.currentMatch)
+    this.currentMatch = op.match
+
+    return this // enable chaining
+  }
+
+  // closes scope of most recent and
+  dna () {
+
+    // pop scope stack & validate that current scope is 'and'
+    const lastAndOrElem = this.andOrElemStack.pop()
+    if (lastAndOrElem !== 'and') {
+      if (lastAndOrElem === undefined) {
+        throw new Error('cant dna without anding first!')
+      }
+      this.andOrElemStack.push(lastAndOrElem) // restore stack before throw
+      throw new Error(`cant dna until ${lastAndOrElem} is closed`)
+    }
+
+    // pop match stack and restore last match to current
+    this.currentMatch = this.matchStack.pop()
+
+    // validate restored match list
+    if (this.currentMatch === undefined) {
+      throw new Error('match stack underflow!')
+    }
+
+    return this // enable chaining
+  }
+
+  // following path segments will be ored
+  or () {
+    const op = { op: 'or', match: [] }
+    this.currentMatch.push(op)
+
+    // push 'or' onto and or elem stack
+    this.andOrElemStack.push('or')
+
+    // push old match list onto match stack & set new ops match as current
+    this.matchStack.push(this.currentMatch)
+    this.currentMatch = op.match
+
+    return this // enable chaining
+  }
+
+  // closes scope of most recent or
+  ro () {
+
+    // pop scope stack & validate that current scope is 'or'
+    const lastAndOrElem = this.andOrElemStack.pop()
+    if (lastAndOrElem !== 'or') {
+      if (lastAndOrElem === undefined) {
+        throw new Error('cant ro without oring first!')
+      }
+      this.andOrElemStack.push(lastAndOrElem) // restore stack before throw
+      throw new Error(`cant ro until ${lastAndOrElem} is closed`)
+    }
+
+    // pop match stack and restore last match to current
+    this.currentMatch = this.matchStack.pop()
+
+    // validate restored match list
+    if (this.currentMatch === undefined) {
+      throw new Error('match stack underflow!')
+    }
+
+    return this // enable chaining
+  }
+
+  equals (value) { // @leaf `{$eq: a}`
+    const op = { op: 'equals', param: this.cloneWhere(), value: value }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  contains (value) { // @leaf `{$contains: a}`
+    const op = { op: 'contains', param: this.cloneWhere(), value: value }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  regex (value) { // @leaf `{$regex: a}`
+    const op = { op: 'regex', param: this.cloneWhere(), value: value }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  exists (flag) { // @leaf `{$eq: a}`
+    const does = flag === true || flag === undefined // defaults to true
+    const op = { op: 'exists', param: this.cloneWhere(), value: does }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  in (...values) { // @leaf `{$in: [one, two, five]}`
+    const op = { op: 'in', param: this.cloneWhere(), values: values }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  gt (value) { // @leaf `{$gt: a}`
+    const op = { op: 'gt', param: this.cloneWhere(), value: value }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  lt (value) { // @leaf `{$lt: a}`
+    const op = { op: 'lt', param: this.cloneWhere(), value: value }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  // *** list operators ***
+  //   -> subtype of match tree nodes
+  //   -> most recent .where('param') path must be list for these to match
+
+  // searches for a single element of list matching *all* following conditions
+  // between elem .. mele nodes where('path') calls are relative to
+  // param path scope at opening of element match
+  elem () {
+    const op = { op: 'elem', param: this.cloneWhere(), match: [] }
+    this.currentMatch.push(op)
+
+    // push current where onto where stack & set where to empty list
+    this.whereStack.push(this.currentWhere)
+    this.currentWhere = []
+
+    // push 'or' onto and or elem stack
+    this.andOrElemStack.push('elem')
+
+    // push old match list onto match stack & set new ops match as current
+    this.matchStack.push(this.currentMatch)
+    this.currentMatch = op.match
+
+    return this // enable chaining
+  }
+
+  mele () {
+
+    // pop scope stack & validate that current scope is 'elem'
+    const lastAndOrElem = this.andOrElemStack.pop()
+    if (lastAndOrElem !== 'elem') {
+      if (lastAndOrElem === undefined) {
+        throw new Error('cant mele without eleming first!')
+      }
+      this.andOrElemStack.push(lastAndOrElem) // restore stack before throw
+      throw new Error(`cant mele until ${lastAndOrElem} is closed`)
+    }
+
+    // pop where stack and restore last where to current
+    this.currentWhere = this.whereStack.pop()
+
+    // pop match stack and restore last match to current
+    this.currentMatch = this.matchStack.pop()
+
+    // validate restored match list
+    if (this.currentMatch === undefined) {
+      throw new Error('match stack underflow!')
+    }
+
+    return this // enable chaining
+  }
+
+  // matches a list that is a superset of given list
+  all (...values) { // @leaf `{$all: [one, two, five]}`
+    const op = { op: 'all', param: this.cloneWhere(), values: values }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  // matches list with *exactly* count items
+  size (count) { // @leaf `{$size: a}`
+    const op = { op: 'size', param: this.cloneWhere(), value: count }
+    this.currentMatch.push(op)
+    return this // enable chaining
+  }
+
+  // *** helper functions ***
+
+  cloneWhere () {
+    if (!Array.isArray(this.currentWhere)) {
+      throw new Error('where value not set!')
+    }
+    return cloneDeep(this.currentWhere)
+  }
+
+  // split parameter path on '.' if there are any
+  static splitParam (param) {
+    return param.split('.')
+  }
+}
+
@dataparty/api
\ No newline at end of file diff --git a/docs/service_index.js.html b/docs/service_index.js.html new file mode 100644 index 0000000..58100fb --- /dev/null +++ b/docs/service_index.js.html @@ -0,0 +1,56 @@ +Source: service/index.js
On this page

service_index.js

const Path = require('path')
+
+/**
+ * @module Service
+ */
+
+
+exports.IContext= require('./icontext')
+exports.IService= require('./iservice')
+exports.IEndpoint= require('./iendpoint')
+exports.IMiddleware= require('./imiddleware')
+exports.ServiceHost= require('./service-host')
+exports.RunnerRouter= require('./runner-router')
+exports.ServiceRunner= require('./service-runner')
+exports.ServiceRunnerNode= require('./service-runner-node')
+exports.EndpointRunner= require('./endpoint-runner')
+exports.EndpointContext= require('./endpoint-context')
+exports.MiddlewareRunner= require('./middleware-runner')
+
+exports.middleware = {
+  pre: {
+    decrypt: require('./middleware/pre/decrypt'),
+    validate: require('./middleware/pre/validate')
+  },
+  post: {
+    validate: require('./middleware/post/validate.js'),
+    encrypt: require('./middleware/post/encrypt')
+  }
+}
+
+exports.middleware_paths = {
+  pre: {
+    decrypt: Path.join(__dirname, './middleware/pre/decrypt.js'),
+    validate: Path.join(__dirname, './middleware/pre/validate.js')
+  },
+  post: {
+    validate: Path.join(__dirname, './middleware/post/validate.js'),
+    encrypt: Path.join(__dirname, './middleware/post/encrypt.js')
+  }
+}
+
+exports.endpoint = {
+  echo: require('./endpoints/echo'),
+  secureecho: require('./endpoints/secure-echo'),
+  identity: require('./endpoints/service-identity'),
+  version: require('./endpoints/service-version')
+}
+
+exports.endpoint_paths = {
+  echo: Path.join(__dirname, './endpoints/echo.js'),
+  secureecho: Path.join(__dirname, './endpoints/secure-echo.js'),
+  identity: Path.join(__dirname, './endpoints/service-identity.js'),
+  version: Path.join(__dirname, './endpoints/service-version.js')
+}
@dataparty/api
\ No newline at end of file