├── .gitignore ├── package.json ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | sandbox.js 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hyperspace/client", 3 | "version": "1.18.0", 4 | "description": "Standalone Hyperspace RPC client", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/hyperspace-org/hyperspace.git" 12 | }, 13 | "keywords": [ 14 | "hypercore", 15 | "hyperspace", 16 | "rpc" 17 | ], 18 | "author": "Hypercore Protocol Team", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/hyperspace-org/hyperspace/issues" 22 | }, 23 | "homepage": "https://github.com/hyperspace-org/hyperspace#readme", 24 | "dependencies": { 25 | "@hyperspace/rpc": "^1.8.0", 26 | "call-me-maybe": "^1.0.1", 27 | "codecs": "^2.1.0", 28 | "freemap": "^1.0.0", 29 | "hypercore-streams": "^1.0.0", 30 | "inspect-custom-symbol": "^1.1.1", 31 | "nanoresource-promise": "^1.2.2" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hypercore Protocol Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @hyperspace/client 2 | 3 | Standalone Hyperspace RPC client 4 | 5 | ``` 6 | npm install @hyperspace/client 7 | ``` 8 | 9 | # Usage 10 | 11 | ``` js 12 | const HyperspaceClient = require('@hyperspace/client') 13 | 14 | const client = new HyperspaceClient() // connect to the Hyperspace server 15 | 16 | const corestore = client.corestore() // make a corestore 17 | 18 | const feed = corestore.get(someHypercoreKey) // make a hypercore 19 | 20 | await feed.get(42) // get some data from the hypercore 21 | ``` 22 | 23 | # API 24 | 25 | #### `const client = new HyperspaceClient([options])` 26 | 27 | Make a new Hyperspace RPC client. Options include: 28 | 29 | ``` js 30 | { 31 | host: 'hyperspace', // the ipc name of the running server 32 | // defaults to hyperspace 33 | port // a TCP port to connect to 34 | } 35 | ``` 36 | 37 | If `port` is specified, or `host` and `port` are both specified, then the client will attempt to connect over TCP. 38 | 39 | If you only provide a `host` option, then it will be considered a Unix socket name. 40 | 41 | #### `await HyperspaceClient.serverReady([host])` 42 | 43 | Static method to wait for the local IPC server to be up and running. 44 | 45 | #### `status = await client.status([callback])` 46 | 47 | Get status of the local daemon. Includes stuff like API version etc. 48 | 49 | #### `await client.close([callback])` 50 | 51 | Fully close the client. Cancels all inflight requests. 52 | 53 | #### `await client.ready([callback])` 54 | 55 | Wait for the client to have fully connected and loaded initial data. 56 | 57 | #### `corestore = client.corestore([namespace])` 58 | 59 | Make a new remote corestore. Optionally you can pass a specific namespace 60 | to load a specific corestore. If you do not pass a namespace a random one is generated for you. 61 | 62 | #### `client.network` 63 | 64 | The remote corestore network instance. 65 | 66 | #### `client.replicate(core)` 67 | 68 | A one-line replication function for `RemoteHypercores` (see below for details). 69 | 70 | ## Remote Corestore 71 | 72 | The remote corestore instances has an API that mimicks the normal [corestore](https://github.com/andrewosh/corestore) API. 73 | 74 | #### `feed = corestore.get([key])` 75 | 76 | Make a new remote hypercore instance. If you pass a key that specific feed is loaded, if not a new one is made. 77 | 78 | #### `feed = corestore.default()` 79 | 80 | Get the "default" feed for this corestore, which is derived from the namespace. 81 | 82 | #### `feed.name` 83 | 84 | The name (namespace) of this corestore. 85 | 86 | #### `async feed.close([callback])` 87 | 88 | Close the corestore. Closes all feeds made in this corestore. 89 | 90 | ## Remote Networker 91 | 92 | The remote networker instance has an API that mimicks the normal [corestore networker](https://github.com/andrewosh/corestore-networker) API. 93 | 94 | #### `await network.ready([callback])` 95 | 96 | Make sure all the peer state is loaded locally. `client.ready` calls this for you. 97 | Note you do not have to call this before using any of the apis, this just makes sure network.peers is populated. 98 | 99 | #### `networks.peers` 100 | 101 | A list of peers we are connected to. 102 | 103 | #### `network.on('peer-add', peer)` 104 | 105 | Emitted when a peer is added. 106 | 107 | #### `network.on('peer-remove', peer)` 108 | 109 | Emitted when a peer is removed. 110 | 111 | #### `await network.configure(discoveryKey | RemoteHypercore, options)` 112 | 113 | Configure the network for this specific discovery key or RemoteHypercore. 114 | Options include: 115 | 116 | ``` 117 | { 118 | lookup: true, // should we find peers? 119 | announce: true, // should we announce ourself as a peer? 120 | flush: true // wait for the full swarm flush before returning? 121 | remember: false // persist this configuration so it stays around after we close our session? 122 | } 123 | ``` 124 | 125 | #### `const ext = network.registerExtension(name, { encoding, onmessage, onerror })` 126 | 127 | Register a network protocol extension. 128 | 129 | ## Remote Feed 130 | 131 | The remote feed instances has an API that mimicks the normal [Hypercore](https://github.com/hypercore-protocol/hypercore) API. 132 | 133 | #### `feed.key` 134 | 135 | The feed public key 136 | 137 | #### `feed.discoveryKey` 138 | 139 | The feed discovery key. 140 | 141 | #### `feed.writable` 142 | 143 | Boolean indicating if this feed is writable. 144 | 145 | #### `await feed.ready([callback])` 146 | 147 | Wait for the key, discoveryKey, writability, initial peers to be loaded. 148 | 149 | #### `const block = await feed.get(index, [options], [callback])` 150 | 151 | Get a block of data from the feed. 152 | 153 | Options include: 154 | 155 | ``` 156 | { 157 | ifAvailable: true, 158 | wait: false, 159 | onwait () { ... } 160 | } 161 | ``` 162 | 163 | See the [Hypercore docs](https://github.com/hypercore-protocol/hypercore) for more info on these options. 164 | 165 | Note if you don't await the promise straight away you can use it to to cancel the operation, later using `feed.cancel` 166 | 167 | ``` js 168 | const p = feed.get(42) 169 | // ... cancel the get 170 | feed.cancel(p) 171 | await p // Was cancelled 172 | ``` 173 | 174 | #### `feed.cancel(p)` 175 | 176 | Cancel a get 177 | 178 | #### `await feed.has(index, [callback])` 179 | 180 | Check if the feed has a specific block 181 | 182 | #### `await feed.download(start, end, [callback])` 183 | 184 | Select a range to be downloaded. 185 | Similarly to `feed.get` you can use the promise itself 186 | to cancel a download using `feed.undownload(p)` 187 | 188 | #### `feed.undownload(p)` 189 | 190 | Stop downloading a range. 191 | 192 | #### `await feed.update([options], [callback])` 193 | 194 | Fetch an update for the feed. 195 | 196 | Options include: 197 | 198 | ``` js 199 | { 200 | minLength: ..., // some min length to update to 201 | ifAvailable: true, 202 | hash: true 203 | } 204 | ``` 205 | 206 | See the [Hypercore docs](https://github.com/hypercore-protocol/hypercore) for more info on these options. 207 | 208 | #### `await feed.append(blockOrArrayOfBlocks, [callback])` 209 | 210 | Append a block or array of blocks to the hypercore 211 | 212 | #### `feed.peers` 213 | 214 | A list of peers this feed is connected to. 215 | 216 | #### `feed.on('peer-add', peer)` 217 | 218 | Emitted when a peer is added. 219 | 220 | #### `feed.on('peer-remove', peer)` 221 | 222 | Emitted when a peer is removed. 223 | 224 | #### `feed.on('append')` 225 | 226 | Emitted when the feed is appended to, either locally or remotely. 227 | 228 | #### `feed.on('download', seq, data)` 229 | 230 | Emitted when a block is downloaded. `data` is a pseudo-buffer with `{length, byteLength}` but no buffer content. 231 | 232 | #### `feed.on('upload', seq, data)` 233 | 234 | Emitted when a block is uploaded. `data` is a pseudo-buffer with `{length, byteLength}` but no buffer content. 235 | 236 | ## Replicator 237 | 238 | Hyperspace also includes a simple replication function for `RemoteHypercores` that does two things: 239 | 1. It first configures the network (`client.network.configure(core, { announce: true, lookup: true })`) 240 | 2. Then it does a `core.update({ ifAvailable: true })` to try to fetch the latest length from the network. 241 | 242 | This saves a bit of time when swarming a `RemoteHypercore`. 243 | 244 | #### `await replicate(core)` 245 | 246 | Quickly connect a `RemoteHypercore` to the Hyperswarm network. 247 | 248 | # License 249 | 250 | MIT 251 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events') 2 | const maybe = require('call-me-maybe') 3 | const codecs = require('codecs') 4 | const inspect = require('inspect-custom-symbol') 5 | const FreeMap = require('freemap') 6 | const { WriteStream, ReadStream } = require('hypercore-streams') 7 | const PROMISES = Symbol.for('hypercore.promises') 8 | 9 | const { NanoresourcePromise: Nanoresource } = require('nanoresource-promise/emitter') 10 | const HRPC = require('@hyperspace/rpc') 11 | const getNetworkOptions = require('@hyperspace/rpc/socket') 12 | const net = require('net') 13 | 14 | class Sessions { 15 | constructor () { 16 | this._cores = new FreeMap() 17 | this._resourceCounter = 0 18 | } 19 | 20 | create (remoteCore) { 21 | return this._cores.add(remoteCore) 22 | } 23 | 24 | createResourceId () { 25 | return this._resourceCounter++ 26 | } 27 | 28 | delete (id) { 29 | this._cores.free(id) 30 | } 31 | 32 | get (id) { 33 | return this._cores.get(id) 34 | } 35 | } 36 | 37 | class RemoteCorestore extends EventEmitter { 38 | constructor (opts = {}) { 39 | super() 40 | 41 | this.name = opts.name || null 42 | this._client = opts.client 43 | this._sessions = opts.sessions || new Sessions() 44 | this._feeds = new Map() 45 | 46 | this._client.hypercore.onRequest(this, { 47 | onAppend ({ id, length, byteLength }) { 48 | const remoteCore = this._sessions.get(id) 49 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 50 | remoteCore._onappend({ length, byteLength }) 51 | }, 52 | onClose ({ id }) { 53 | const remoteCore = this._sessions.get(id) 54 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 55 | remoteCore.close(() => {}) // no unhandled rejects 56 | }, 57 | onPeerOpen ({ id, peer }) { 58 | const remoteCore = this._sessions.get(id) 59 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 60 | remoteCore._onpeeropen(peer) 61 | }, 62 | onPeerRemove ({ id, peer }) { 63 | const remoteCore = this._sessions.get(id) 64 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 65 | remoteCore._onpeerremove(peer) 66 | }, 67 | onExtension ({ id, resourceId, remotePublicKey, data }) { 68 | const remoteCore = this._sessions.get(id) 69 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 70 | remoteCore._onextension({ resourceId, remotePublicKey, data }) 71 | }, 72 | onWait ({ id, onWaitId, seq }) { 73 | const remoteCore = this._sessions.get(id) 74 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 75 | remoteCore._onwait(onWaitId, seq) 76 | }, 77 | onDownload ({ id, seq, byteLength }) { 78 | const remoteCore = this._sessions.get(id) 79 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 80 | remoteCore._ondownload({ seq, byteLength }) 81 | }, 82 | onUpload ({ id, seq, byteLength }) { 83 | const remoteCore = this._sessions.get(id) 84 | if (!remoteCore) throw new Error('Invalid RemoteHypercore ID.') 85 | remoteCore._onupload({ seq, byteLength }) 86 | } 87 | }) 88 | this._client.corestore.onRequest(this, { 89 | onFeed ({ key }) { 90 | return this._onfeed(key) 91 | } 92 | }) 93 | } 94 | 95 | // Events 96 | 97 | _onfeed (key) { 98 | if (!this.listenerCount('feed')) return 99 | this.emit('feed', this.get(key, { weak: true, lazy: true })) 100 | } 101 | 102 | // Public Methods 103 | 104 | replicate () { 105 | throw new Error('Cannot call replicate on a RemoteCorestore') 106 | } 107 | 108 | default (opts = {}) { 109 | return this.get(opts.key, { name: this.name }) 110 | } 111 | 112 | get (key, opts = {}) { 113 | if (key && typeof key !== 'string' && !Buffer.isBuffer(key)) { 114 | opts = key 115 | key = opts.key 116 | } 117 | if (typeof key === 'string') key = Buffer.from(key, 'hex') 118 | 119 | let hex = key && key.toString('hex') 120 | if (hex && this._feeds.has(hex)) return this._feeds.get(hex) 121 | 122 | const feed = new RemoteHypercore(this._client, this._sessions, key, opts) 123 | 124 | if (hex) { 125 | this._feeds.set(hex, feed) 126 | } else { 127 | feed.on('ready', () => { 128 | hex = feed.key.toString('hex') 129 | if (!this._feeds.has(hex)) this._feeds.set(hex, feed) 130 | }) 131 | } 132 | 133 | feed.on('close', () => { 134 | if (hex && this._feeds.get(hex) === feed) this._feeds.delete(hex) 135 | }) 136 | 137 | return feed 138 | } 139 | 140 | namespace (name) { 141 | return new this.constructor({ 142 | client: this._client, 143 | sessions: this._sessions, 144 | name: name || randomNamespace() 145 | }) 146 | } 147 | 148 | ready (cb) { 149 | if (cb) process.nextTick(cb, null) 150 | } 151 | 152 | async _closeAll () { 153 | const proms = [] 154 | for (const [k, feed] of this._feeds) { 155 | this._feeds.delete(k) 156 | proms.push(feed.close()) 157 | } 158 | 159 | try { 160 | await Promise.all(proms) 161 | } catch (err) { 162 | await Promise.allSettled(proms) 163 | throw err 164 | } 165 | } 166 | 167 | close (cb) { 168 | return maybeOptional(cb, this._closeAll()) 169 | } 170 | } 171 | 172 | class RemoteNetworker extends EventEmitter { 173 | constructor (opts) { 174 | super() 175 | this._client = opts.client 176 | this._sessions = opts.sessions 177 | this._extensions = new Map() 178 | 179 | this.peers = null 180 | this.publicKey = null 181 | 182 | this._client.network.onRequest(this, { 183 | onPeerAdd: this._onpeeradd.bind(this), 184 | onPeerRemove: this._onpeerremove.bind(this), 185 | onExtension: this._onextension.bind(this) 186 | }) 187 | 188 | this.ready().catch(noop) 189 | } 190 | 191 | // Event Handlers 192 | 193 | _onpeeradd ({ peer }) { 194 | this.peers.push(peer) 195 | this.emit('peer-open', peer) 196 | this.emit('peer-add', peer) 197 | } 198 | 199 | _onpeerremove ({ peer }) { 200 | const idx = this._indexOfPeer(peer.remotePublicKey) 201 | if (idx === -1) return 202 | this.peers[idx] = this.peers[this.peers.length - 1] 203 | this.peers.pop() 204 | this.emit('peer-remove', peer) 205 | } 206 | 207 | _onextension ({ resourceId, remotePublicKey, data }) { 208 | const idx = this._indexOfPeer(remotePublicKey) 209 | if (idx === -1) return 210 | const remotePeer = this.peers[idx] 211 | const ext = this._extensions.get(resourceId) 212 | if (ext.destroyed) return 213 | ext.onmessage(data, remotePeer) 214 | } 215 | 216 | // Private Methods 217 | 218 | _indexOfPeer (remotePublicKey) { 219 | for (let i = 0; i < this.peers.length; i++) { 220 | if (remotePublicKey.equals(this.peers[i].remotePublicKey)) return i 221 | } 222 | return -1 223 | } 224 | 225 | async _open () { 226 | if (this.peers) return null 227 | const rsp = await this._client.network.open() 228 | this.peers = rsp.peers 229 | this.keyPair = { 230 | publicKey: rsp.publicKey, 231 | privateKey: null 232 | } 233 | } 234 | 235 | async _configure (discoveryKey, opts) { 236 | if (typeof discoveryKey === 'object' && !Buffer.isBuffer(discoveryKey)) { 237 | const core = discoveryKey 238 | if (!core.discoveryKey) await core.ready() 239 | discoveryKey = core.discoveryKey 240 | } 241 | return this._client.network.configure({ 242 | configuration: { 243 | discoveryKey, 244 | announce: opts.announce, 245 | lookup: opts.lookup, 246 | remember: opts.remember 247 | }, 248 | flush: opts.flush, 249 | copyFrom: opts.copyFrom, 250 | overwrite: opts.overwrite 251 | }) 252 | } 253 | 254 | // Public Methods 255 | 256 | ready (cb) { 257 | return maybe(cb, this._open()) 258 | } 259 | 260 | configure (discoveryKey, opts = {}, cb) { 261 | const configureProm = this._configure(discoveryKey, opts) 262 | maybeOptional(cb, configureProm) 263 | return configureProm 264 | } 265 | 266 | async status (discoveryKey, cb) { 267 | return maybe(cb, (async () => { 268 | const rsp = await this._client.network.status({ 269 | discoveryKey 270 | }) 271 | return rsp.status 272 | })()) 273 | } 274 | 275 | async allStatuses (cb) { 276 | return maybe(cb, (async () => { 277 | const rsp = await this._client.network.allStatuses() 278 | return rsp.statuses 279 | })()) 280 | } 281 | 282 | registerExtension (name, opts) { 283 | const ext = new RemoteNetworkerExtension(this, name, opts) 284 | this._extensions.set(ext.resourceId, ext) 285 | return ext 286 | } 287 | } 288 | 289 | class RemoteNetworkerExtension { 290 | constructor (networker, name, opts = {}) { 291 | if (typeof name === 'object') { 292 | opts = name 293 | name = opts.name 294 | } 295 | this.networker = networker 296 | this.resourceId = networker._sessions.createResourceId() 297 | this.name = name 298 | this.encoding = codecs((opts && opts.encoding) || 'binary') 299 | this.destroyed = false 300 | 301 | this.onerror = opts.onerror || noop 302 | this.onmessage = noop 303 | if (opts.onmessage) { 304 | this.onmessage = (message, peer) => { 305 | try { 306 | message = this.encoding.decode(message) 307 | } catch (err) { 308 | return this.onerror(err) 309 | } 310 | return opts.onmessage(message, peer) 311 | } 312 | } 313 | 314 | this.networker._client.network.registerExtensionNoReply({ 315 | id: 0, 316 | resourceId: this.resourceId, 317 | name: this.name 318 | }) 319 | } 320 | 321 | broadcast (message) { 322 | if (this.destroyed) return 323 | const buf = this.encoding.encode(message) 324 | this.networker._client.network.sendExtensionNoReply({ 325 | id: 0, 326 | resourceId: this.resourceId, 327 | remotePublicKey: null, 328 | data: buf 329 | }) 330 | } 331 | 332 | send (message, peer) { 333 | if (this.destroyed) return 334 | const buf = this.encoding.encode(message) 335 | this.networker._client.network.sendExtensionNoReply({ 336 | id: 0, 337 | resourceId: this.resourceId, 338 | remotePublicKey: peer.remotePublicKey, 339 | data: buf 340 | }) 341 | } 342 | 343 | destroy () { 344 | this.destroyed = true 345 | this.networker._client.network.unregisterExtensionNoReply({ 346 | id: 0, 347 | resourceId: this.resourceId 348 | }, (err) => { 349 | if (err) this.onerror(err) 350 | this.networker._extensions.delete(this.resourceId) 351 | }) 352 | } 353 | } 354 | 355 | class RemoteHypercore extends Nanoresource { 356 | constructor (client, sessions, key, opts) { 357 | super() 358 | this.key = key 359 | this.discoveryKey = null 360 | this.length = 0 361 | this.byteLength = 0 362 | this.writable = false 363 | this.sparse = true 364 | this.peers = [] 365 | this.valueEncoding = null 366 | if (opts.valueEncoding) { 367 | if (typeof opts.valueEncoding === 'string') this.valueEncoding = codecs(opts.valueEncoding) 368 | else this.valueEncoding = opts.valueEncoding 369 | } 370 | 371 | this.weak = !!opts.weak 372 | this.lazy = !!opts.lazy 373 | this[PROMISES] = true 374 | 375 | this._client = client 376 | this._sessions = sessions 377 | this._name = opts.name 378 | this._id = this.lazy ? undefined : this._sessions.create(this) 379 | this._extensions = new Map() 380 | this._onwaits = new FreeMap(1) 381 | 382 | // Track listeners for some events and enable/disable watching. 383 | this.on('newListener', (event) => { 384 | if (event === 'download' && !this.listenerCount(event)) { 385 | this._watchDownloads() 386 | } 387 | if (event === 'upload' && !this.listenerCount(event)) { 388 | this._watchUploads() 389 | } 390 | }) 391 | this.on('removeListener', (event) => { 392 | if (event === 'download' && !this.listenerCount(event)) { 393 | this._unwatchDownloads() 394 | } 395 | if (event === 'upload' && !this.listenerCount(event)) { 396 | this._unwatchUploads() 397 | } 398 | }) 399 | 400 | if (!this.lazy) this.ready(() => {}) 401 | 402 | if (this.sparse && opts.eagerUpdate) { 403 | const self = this 404 | this.update({ ifAvailable: false }, function loop (err) { 405 | if (err) self.emit('update-error', err) 406 | self.update(loop) 407 | }) 408 | } 409 | } 410 | 411 | ready (cb) { 412 | return maybe(cb, this.open()) 413 | } 414 | 415 | [inspect] (depth, opts) { 416 | var indent = '' 417 | if (typeof opts.indentationLvl === 'number') { 418 | while (indent.length < opts.indentationLvl) indent += ' ' 419 | } 420 | return 'RemoteHypercore(\n' + 421 | indent + ' key: ' + opts.stylize(this.key && this.key.toString('hex'), 'string') + '\n' + 422 | indent + ' discoveryKey: ' + opts.stylize(this.discoveryKey && this.discoveryKey.toString('hex'), 'string') + '\n' + 423 | indent + ' opened: ' + opts.stylize(this.opened, 'boolean') + '\n' + 424 | indent + ' writable: ' + opts.stylize(this.writable, 'boolean') + '\n' + 425 | indent + ' length: ' + opts.stylize(this.length, 'number') + '\n' + 426 | indent + ' byteLength: ' + opts.stylize(this.byteLength, 'number') + '\n' + 427 | indent + ' peers: ' + opts.stylize(this.peers.length, 'number') + '\n' + 428 | indent + ')' 429 | } 430 | 431 | // Nanoresource Methods 432 | 433 | async _open () { 434 | if (this.lazy) this._id = this._sessions.create(this) 435 | const rsp = await this._client.corestore.open({ 436 | id: this._id, 437 | name: this._name, 438 | key: this.key, 439 | weak: this.weak 440 | }) 441 | this.key = rsp.key 442 | this.discoveryKey = rsp.discoveryKey 443 | this.writable = rsp.writable 444 | this.length = rsp.length 445 | this.byteLength = rsp.byteLength 446 | if (rsp.peers) this.peers = rsp.peers 447 | this.emit('ready') 448 | } 449 | 450 | async _close () { 451 | await this._client.hypercore.close({ id: this._id }) 452 | this._sessions.delete(this._id) 453 | this.emit('close') 454 | } 455 | 456 | // Events 457 | 458 | _onwait (id, seq) { 459 | const onwait = this._onwaits.get(id) 460 | if (onwait) { 461 | this._onwaits.free(id) 462 | onwait(seq) 463 | } 464 | } 465 | 466 | _onappend (rsp) { 467 | this.length = rsp.length 468 | this.byteLength = rsp.byteLength 469 | this.emit('append') 470 | } 471 | 472 | _onpeeropen (peer) { 473 | const remotePeer = new RemoteHypercorePeer(peer.type, peer.remoteAddress, peer.remotePublicKey) 474 | this.peers.push(remotePeer) 475 | this.emit('peer-add', remotePeer) // compat 476 | this.emit('peer-open', remotePeer) 477 | } 478 | 479 | _onpeerremove (peer) { 480 | const idx = this._indexOfPeer(peer.remotePublicKey) 481 | if (idx === -1) throw new Error('A peer was removed that was not previously added.') 482 | const remotePeer = this.peers[idx] 483 | this.peers.splice(idx, 1) 484 | this.emit('peer-remove', remotePeer) 485 | } 486 | 487 | _onextension ({ resourceId, remotePublicKey, data }) { 488 | const idx = this._indexOfPeer(remotePublicKey) 489 | if (idx === -1) return 490 | const remotePeer = this.peers[idx] 491 | const ext = this._extensions.get(resourceId) 492 | if (ext.destroyed) return 493 | ext.onmessage(data, remotePeer) 494 | } 495 | 496 | _ondownload (rsp) { 497 | // TODO: Add to local bitfield? 498 | this.emit('download', rsp.seq, {length: rsp.byteLength, byteLength: rsp.byteLength}) 499 | } 500 | 501 | _onupload (rsp) { 502 | // TODO: Add to local bitfield? 503 | this.emit('upload', rsp.seq, {length: rsp.byteLength, byteLength: rsp.byteLength}) 504 | } 505 | 506 | // Private Methods 507 | 508 | _indexOfPeer (remotePublicKey) { 509 | for (let i = 0; i < this.peers.length; i++) { 510 | if (remotePublicKey.equals(this.peers[i].remotePublicKey)) return i 511 | } 512 | return -1 513 | } 514 | 515 | async _append (blocks) { 516 | if (!this.opened) await this.open() 517 | if (this.closed) throw new Error('Feed is closed') 518 | 519 | if (!Array.isArray(blocks)) blocks = [blocks] 520 | if (this.valueEncoding) blocks = blocks.map(b => this.valueEncoding.encode(b)) 521 | const rsp = await this._client.hypercore.append({ 522 | id: this._id, 523 | blocks 524 | }) 525 | return rsp.seq 526 | } 527 | 528 | async _get (seq, opts, resourceId) { 529 | if (!this.opened) await this.open() 530 | if (this.closed) throw new Error('Feed is closed') 531 | 532 | let onWaitId = 0 533 | const onwait = opts && opts.onwait 534 | if (onwait) onWaitId = this._onwaits.add(onwait) 535 | 536 | let rsp 537 | 538 | try { 539 | rsp = await this._client.hypercore.get({ 540 | ...opts, 541 | seq, 542 | id: this._id, 543 | resourceId, 544 | onWaitId 545 | }) 546 | } finally { 547 | if (onWaitId !== 0 && onwait === this._onwaits.get(onWaitId)) this._onwaits.free(onWaitId) 548 | } 549 | 550 | if (opts && opts.valueEncoding) return codecs(opts.valueEncoding).decode(rsp.block) 551 | if (this.valueEncoding) return this.valueEncoding.decode(rsp.block) 552 | return rsp.block 553 | } 554 | 555 | async _cancel (resourceId) { 556 | try { 557 | if (!this.opened) await this.open() 558 | if (this.closed) return 559 | } catch (_) {} 560 | 561 | this._client.hypercore.cancelNoReply({ 562 | id: this._id, 563 | resourceId 564 | }) 565 | } 566 | 567 | async _update (opts) { 568 | if (!this.opened) await this.open() 569 | if (this.closed) throw new Error('Feed is closed') 570 | 571 | if (typeof opts === 'number') opts = { minLength: opts } 572 | if (!opts) opts = {} 573 | if (typeof opts.minLength !== 'number') opts.minLength = this.length + 1 574 | return await this._client.hypercore.update({ 575 | ...opts, 576 | id: this._id 577 | }) 578 | } 579 | 580 | async _seek (byteOffset, opts) { 581 | if (!this.opened) await this.open() 582 | if (this.closed) throw new Error('Feed is closed') 583 | 584 | const rsp = await this._client.hypercore.seek({ 585 | byteOffset, 586 | ...opts, 587 | id: this._id 588 | }) 589 | return { 590 | seq: rsp.seq, 591 | blockOffset: rsp.blockOffset 592 | } 593 | } 594 | 595 | async _has (seq) { 596 | if (!this.opened) await this.open() 597 | if (this.closed) throw new Error('Feed is closed') 598 | 599 | const rsp = await this._client.hypercore.has({ 600 | seq, 601 | id: this._id 602 | }) 603 | return rsp.has 604 | } 605 | 606 | async _download (range, resourceId) { 607 | if (!this.opened) await this.open() 608 | if (this.closed) throw new Error('Feed is closed') 609 | 610 | return this._client.hypercore.download({ ...range, id: this._id, resourceId }) 611 | } 612 | 613 | async _undownload (resourceId) { 614 | try { 615 | if (!this.opened) await this.open() 616 | if (this.closed) return 617 | } catch (_) {} 618 | 619 | return this._client.hypercore.undownloadNoReply({ id: this._id, resourceId }) 620 | } 621 | 622 | async _downloaded (start, end) { 623 | if (!this.opened) await this.open() 624 | if (this.closed) throw new Error('Feed is closed') 625 | const rsp = await this._client.hypercore.downloaded({ id: this._id, start, end }) 626 | return rsp.bytes 627 | } 628 | 629 | async _watchDownloads () { 630 | try { 631 | if (!this.opened) await this.open() 632 | if (this.closed) return 633 | this._client.hypercore.watchDownloadsNoReply({ id: this._id }) 634 | } catch (_) {} 635 | } 636 | 637 | async _unwatchDownloads () { 638 | try { 639 | if (!this.opened) await this.open() 640 | if (this.closed) return 641 | this._client.hypercore.unwatchDownloadsNoReply({ id: this._id }) 642 | } catch (_) {} 643 | } 644 | 645 | async _watchUploads () { 646 | try { 647 | if (!this.opened) await this.open() 648 | if (this.closed) return 649 | this._client.hypercore.watchUploadsNoReply({ id: this._id }) 650 | } catch (_) {} 651 | } 652 | 653 | async _unwatchUploads () { 654 | try { 655 | if (!this.opened) await this.open() 656 | if (this.closed) return 657 | this._client.hypercore.unwatchUploadsNoReply({ id: this._id }) 658 | } catch (_) {} 659 | } 660 | 661 | // Public Methods 662 | 663 | append (blocks, cb) { 664 | return maybeOptional(cb, this._append(blocks)) 665 | } 666 | 667 | get (seq, opts, cb) { 668 | if (typeof opts === 'function') { 669 | cb = opts 670 | opts = null 671 | } 672 | if (!(seq >= 0)) throw new Error('seq must be a positive number') 673 | 674 | const resourceId = this._sessions.createResourceId() 675 | const prom = this._get(seq, opts, resourceId) 676 | prom.resourceId = resourceId 677 | maybe(cb, prom) 678 | return prom 679 | } 680 | 681 | update (opts, cb) { 682 | if (typeof opts === 'function') { 683 | cb = opts 684 | opts = null 685 | } 686 | return maybeOptional(cb, this._update(opts)) 687 | } 688 | 689 | seek (byteOffset, opts, cb) { 690 | if (typeof opts === 'function') { 691 | cb = opts 692 | opts = null 693 | } 694 | const seekProm = this._seek(byteOffset, opts) 695 | if (!cb) return seekProm 696 | seekProm.then( 697 | ({ seq, blockOffset }) => process.nextTick(cb, null, seq, blockOffset), 698 | err => process.nextTick(cb, err) 699 | ) 700 | } 701 | 702 | has (seq, cb) { 703 | return maybe(cb, this._has(seq)) 704 | } 705 | 706 | cancel (get) { 707 | if (typeof get.resourceId !== 'number') throw new Error('Must pass a get return value') 708 | return this._cancel(get.resourceId) 709 | } 710 | 711 | createReadStream (opts) { 712 | return new ReadStream(this, opts) 713 | } 714 | 715 | createWriteStream (opts) { 716 | return new WriteStream(this, opts) 717 | } 718 | 719 | download (range, cb) { 720 | if (typeof range === 'number') range = { start: range, end: range + 1 } 721 | if (!range) range = {} 722 | if (Array.isArray(range)) range = { blocks: range } 723 | 724 | // much easier to run this in the client due to pbuf defaults 725 | if (range.blocks && typeof range.start !== 'number') { 726 | let min = -1 727 | let max = 0 728 | 729 | for (let i = 0; i < range.blocks.length; i++) { 730 | const blk = range.blocks[i] 731 | if (min === -1 || blk < min) min = blk 732 | if (blk >= max) max = blk + 1 733 | } 734 | 735 | range.start = min === -1 ? 0 : min 736 | range.end = max 737 | } 738 | 739 | // massage end = -1, over to something more protobuf friendly 740 | if (range.end === undefined || range.end === -1) { 741 | range.end = 0 742 | range.live = true 743 | } 744 | 745 | const resourceId = this._sessions.createResourceId() 746 | 747 | const prom = this._download(range, resourceId) 748 | prom.catch(noop) // optional promise due to the hypercore signature 749 | prom.resourceId = resourceId 750 | 751 | maybe(cb, prom) 752 | return prom // always return prom as that one is the "cancel" token 753 | } 754 | 755 | undownload (download) { 756 | if (typeof download.resourceId !== 'number') throw new Error('Must pass a download return value') 757 | this._undownload(download.resourceId) 758 | } 759 | 760 | downloaded (start, end, cb) { 761 | if (typeof start === 'function') { 762 | start = null 763 | end = null 764 | cb = start 765 | } else if (typeof end === 'function') { 766 | end = null 767 | cb = end 768 | } 769 | return maybe(cb, this._downloaded(start, end)) 770 | } 771 | 772 | lock (onlocked) { 773 | // TODO: refactor so this can be opened without waiting for open 774 | if (!this.opened) throw new Error('Cannot acquire a lock for an unopened feed') 775 | 776 | const prom = this._client.hypercore.acquireLock({ id: this._id }) 777 | 778 | if (onlocked) { 779 | const release = (cb, err, val) => { // mutexify interface 780 | this._client.hypercore.releaseLockNoReply({ id: this._id }) 781 | if (cb) cb(err, val) 782 | } 783 | 784 | prom.then(() => process.nextTick(onlocked, release), noop) 785 | return 786 | } 787 | 788 | return prom.then(() => () => this._client.hypercore.releaseLockNoReply({ id: this._id })) 789 | } 790 | 791 | // TODO: Unimplemented methods 792 | 793 | registerExtension (name, opts) { 794 | const ext = new RemoteHypercoreExtension(this, name, opts) 795 | this._extensions.set(ext.resourceId, ext) 796 | return ext 797 | } 798 | 799 | replicate () { 800 | throw new Error('Cannot call replicate on a RemoteHyperdrive') 801 | } 802 | } 803 | 804 | class RemoteHypercorePeer { 805 | constructor (type, remoteAddress, remotePublicKey) { 806 | this.type = type 807 | this.remoteAddress = remoteAddress 808 | this.remotePublicKey = remotePublicKey 809 | } 810 | } 811 | 812 | class RemoteHypercoreExtension { 813 | constructor (feed, name, opts = {}) { 814 | if (typeof name === 'object') { 815 | opts = name 816 | name = opts.name 817 | } 818 | this.feed = feed 819 | this.resourceId = feed._sessions.createResourceId() 820 | this.name = name 821 | this.encoding = codecs((opts && opts.encoding) || 'binary') 822 | this.destroyed = false 823 | 824 | this.onerror = opts.onerror || noop 825 | this.onmessage = noop 826 | if (opts.onmessage) { 827 | this.onmessage = (message, peer) => { 828 | try { 829 | message = this.encoding.decode(message) 830 | } catch (err) { 831 | return this.onerror(err) 832 | } 833 | return opts.onmessage(message, peer) 834 | } 835 | } 836 | 837 | const reg = () => { 838 | this.feed._client.hypercore.registerExtensionNoReply({ 839 | id: this.feed._id, 840 | resourceId: this.resourceId, 841 | name: this.name 842 | }) 843 | } 844 | 845 | if (this.feed._id !== undefined) { 846 | reg() 847 | } else { 848 | this.feed.ready((err) => { 849 | if (err) return this.onerror(err) 850 | reg() 851 | }) 852 | } 853 | } 854 | 855 | broadcast (message) { 856 | const buf = this.encoding.encode(message) 857 | if (this.feed._id === undefined || this.destroyed) return 858 | this.feed._client.hypercore.sendExtensionNoReply({ 859 | id: this.feed._id, 860 | resourceId: this.resourceId, 861 | remotePublicKey: null, 862 | data: buf 863 | }) 864 | } 865 | 866 | send (message, peer) { 867 | if (this.feed._id === undefined || this.destroyed) return 868 | const buf = this.encoding.encode(message) 869 | this.feed._client.hypercore.sendExtensionNoReply({ 870 | id: this.feed._id, 871 | resourceId: this.resourceId, 872 | remotePublicKey: peer.remotePublicKey, 873 | data: buf 874 | }) 875 | } 876 | 877 | destroy () { 878 | this.destroyed = true 879 | this.feed.ready((err) => { 880 | if (err) return this.onerror(err) 881 | this.feed._client.hypercore.unregisterExtensionNoReply({ 882 | id: this.feed._id, 883 | resourceId: this.resourceId 884 | }, err => { 885 | if (err) this.onerror(err) 886 | this.feed._extensions.delete(this.resourceId) 887 | }) 888 | }) 889 | } 890 | } 891 | 892 | module.exports = class HyperspaceClient { 893 | constructor (opts = {}) { 894 | const sessions = new Sessions() 895 | 896 | this._socketOpts = getNetworkOptions(opts) 897 | this._client = HRPC.connect(this._socketOpts) 898 | this._corestore = new RemoteCorestore({ client: this._client, sessions }) 899 | 900 | this.network = new RemoteNetworker({ client: this._client, sessions }) 901 | this.corestore = (name) => this._corestore.namespace(name) 902 | // Exposed like this so that you can destructure: const { replicate } = new Client() 903 | this.replicate = (core, cb) => maybeOptional(cb, this._replicate(core)) 904 | } 905 | 906 | static async serverReady (opts) { 907 | const sock = getNetworkOptions(opts) 908 | return new Promise((resolve) => { 909 | retry() 910 | 911 | function retry () { 912 | const socket = net.connect(sock) 913 | let connected = false 914 | 915 | socket.on('connect', function () { 916 | connected = true 917 | socket.destroy() 918 | }) 919 | socket.on('error', socket.destroy) 920 | socket.on('close', function () { 921 | if (connected) return resolve() 922 | setTimeout(retry, 100) 923 | }) 924 | } 925 | }) 926 | } 927 | 928 | status (cb) { 929 | return maybe(cb, this._client.hyperspace.status()) 930 | } 931 | 932 | stop (cb) { 933 | return maybe(cb, this._client.hyperspace.stopNoReply()) 934 | } 935 | 936 | close () { 937 | return this._client.destroy() 938 | } 939 | 940 | ready (cb) { 941 | return maybe(cb, this.network.ready()) 942 | } 943 | 944 | async _replicate (core) { 945 | await this.network.configure(core, { 946 | announce: true, 947 | lookup: true 948 | }) 949 | try { 950 | await core.update({ ifAvailable: true }) 951 | } catch (_) { 952 | // If this update fails, the error can be ignored. 953 | } 954 | } 955 | } 956 | 957 | function noop () {} 958 | 959 | function randomNamespace () { // does *not* have to be secure 960 | let ns = '' 961 | while (ns < 64) ns += Math.random().toString(16).slice(2) 962 | return ns.slice(0, 64) 963 | } 964 | 965 | function maybeOptional (cb, prom) { 966 | prom = maybe(cb, prom) 967 | if (prom) prom.catch(noop) 968 | return prom 969 | } 970 | --------------------------------------------------------------------------------