├── diagram.png ├── core.js ├── .gitignore ├── compat.js ├── scheduler.js ├── src ├── types │ ├── db.ts │ ├── hub.ts │ ├── query.ts │ ├── staging.ts │ └── index.ts ├── index.ts ├── gossip.ts ├── interpool-glue.ts ├── conn.ts └── conn-scheduler.ts ├── test ├── compat.js ├── mock.js ├── index.js └── interpool.js ├── tsconfig.json ├── .github └── workflows │ └── node.js.yml ├── LICENSE ├── package.json ├── CHANGELOG.md └── README.md /diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ssbc/ssb-conn/HEAD/diagram.png -------------------------------------------------------------------------------- /core.js: -------------------------------------------------------------------------------- 1 | const {CONN} = require('./lib/conn'); 2 | module.exports = CONN; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | shrinkwrap.yaml 4 | pnpm-lock.yaml 5 | lib -------------------------------------------------------------------------------- /compat.js: -------------------------------------------------------------------------------- 1 | const {Gossip} = require('./lib/gossip'); 2 | module.exports = Gossip; 3 | -------------------------------------------------------------------------------- /scheduler.js: -------------------------------------------------------------------------------- 1 | const {ConnScheduler} = require('./lib/conn-scheduler'); 2 | module.exports = ConnScheduler; 3 | -------------------------------------------------------------------------------- /src/types/db.ts: -------------------------------------------------------------------------------- 1 | import ConnDB = require('ssb-conn-db'); 2 | export {ConnDB}; 3 | export * from 'ssb-conn-db/lib/types'; 4 | -------------------------------------------------------------------------------- /src/types/hub.ts: -------------------------------------------------------------------------------- 1 | import ConnHub = require('ssb-conn-hub'); 2 | export {ConnHub}; 3 | export * from 'ssb-conn-hub/lib/types'; 4 | -------------------------------------------------------------------------------- /src/types/query.ts: -------------------------------------------------------------------------------- 1 | import ConnQuery = require('ssb-conn-query'); 2 | export {ConnQuery}; 3 | export * from 'ssb-conn-query/lib/types'; 4 | -------------------------------------------------------------------------------- /src/types/staging.ts: -------------------------------------------------------------------------------- 1 | import ConnStaging = require('ssb-conn-staging'); 2 | export {ConnStaging}; 3 | export * from 'ssb-conn-staging/lib/types'; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {Gossip} from './gossip'; 2 | import {CONN} from './conn'; 3 | import {ConnScheduler} from './conn-scheduler'; 4 | 5 | module.exports = [CONN, Gossip, ConnScheduler]; 6 | -------------------------------------------------------------------------------- /test/compat.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const mock = require('./mock'); 3 | 4 | tape('Gossip Ping API is available on SSB', t => { 5 | const ssb = mock(); 6 | t.ok(ssb.gossip, 'ssb.gossip'); 7 | t.ok(ssb.gossip.ping, 'ssb.gossip.ping'); 8 | 9 | t.end(); 10 | }); 11 | -------------------------------------------------------------------------------- /src/gossip.ts: -------------------------------------------------------------------------------- 1 | import {plugin, muxrpc} from 'secret-stack-decorators'; 2 | 3 | @plugin('1.0.0') 4 | export class Gossip { 5 | private readonly ssb: any; 6 | 7 | constructor(ssb: any) { 8 | this.ssb = ssb; 9 | } 10 | 11 | @muxrpc('duplex', {anonymous: 'allow'}) 12 | public ping = () => this.ssb.conn.ping(); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "lib": ["es5", "es2015", "es2016", "es2017"], 6 | "skipLibCheck": true, 7 | "declaration": true, 8 | "outDir": "./lib", 9 | "rootDir": "./src", 10 | "removeComments": true, 11 | "strict": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "downlevelIteration": true, 17 | "moduleResolution": "node", 18 | "experimentalDecorators": true, 19 | "types": ["node", "ziii"], 20 | "esModuleInterop": false 21 | }, 22 | "exclude": ["node_modules", "test", "lib"] 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x, 14.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm i 28 | - run: npm test 29 | -------------------------------------------------------------------------------- /test/mock.js: -------------------------------------------------------------------------------- 1 | const CONN = require('../core'); 2 | const ConnScheduler = require('../scheduler'); 3 | const Gossip = require('../compat'); 4 | const os = require('os'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const Caps = require('ssb-caps'); 8 | 9 | module.exports = function mock() { 10 | const testPath = fs.mkdtempSync(path.join(os.tmpdir(), 'conntest-')); 11 | 12 | const mockSSB = { 13 | addListener() {}, 14 | close: { 15 | hook: () => {}, 16 | }, 17 | post: () => {}, 18 | connect: (_address, cb) => { 19 | setTimeout(() => { 20 | cb(null, {}); 21 | }, 200); 22 | }, 23 | }; 24 | const mockConfig = { 25 | path: testPath, 26 | caps: Caps, 27 | }; 28 | 29 | mockSSB.conn = new CONN(mockSSB, mockConfig); 30 | mockSSB.connScheduler = new ConnScheduler(mockSSB, mockConfig); 31 | mockSSB.gossip = new Gossip(mockSSB, mockConfig); 32 | 33 | return mockSSB; 34 | }; 35 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const mock = require('./mock'); 3 | 4 | tape('CONN APIs are available on SSB', t => { 5 | const ssb = mock(); 6 | t.ok(ssb.conn, 'ssb.conn'); 7 | 8 | t.ok(ssb.conn.remember, 'ssb.conn.remember'); 9 | t.ok(ssb.conn.forget, 'ssb.conn.forget'); 10 | t.ok(ssb.conn.dbPeers, 'ssb.conn.dbPeers'); 11 | 12 | t.ok(ssb.conn.connect, 'ssb.conn.connect'); 13 | t.ok(ssb.conn.disconnect, 'ssb.conn.disconnect'); 14 | t.ok(ssb.conn.peers, 'ssb.conn.peers'); 15 | 16 | t.ok(ssb.conn.stage, 'ssb.conn.stage'); 17 | t.ok(ssb.conn.unstage, 'ssb.conn.unstage'); 18 | t.ok(ssb.conn.stagedPeers, 'ssb.conn.stagedPeers'); 19 | 20 | t.ok(ssb.conn.db, 'ssb.conn.db'); 21 | t.ok(ssb.conn.hub, 'ssb.conn.hub'); 22 | t.ok(ssb.conn.staging, 'ssb.conn.staging'); 23 | t.ok(ssb.conn.query, 'ssb.conn.query'); 24 | t.ok(ssb.conn.start, 'ssb.conn.start'); 25 | t.ok(ssb.conn.stop, 'ssb.conn.stop'); 26 | t.ok(ssb.conn.ping, 'ssb.conn.ping'); 27 | 28 | t.end(); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Andre Staltz 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-conn", 3 | "description": "SSB plugin for establishing and managing peer connections", 4 | "version": "6.0.4", 5 | "homepage": "https://github.com/staltz/ssb-conn", 6 | "main": "lib/index.js", 7 | "types": "lib/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/staltz/ssb-conn.git" 11 | }, 12 | "files": [ 13 | "*.js", 14 | "lib/**/*.js", 15 | "lib/**/*.d.ts" 16 | ], 17 | "dependencies": { 18 | "debug": "^4.3.1", 19 | "has-network2": ">=0.0.3", 20 | "ip": "^1.1.5", 21 | "on-change-network-strict": "1.0.0", 22 | "on-wakeup": "^1.0.1", 23 | "pull-notify": "^0.1.2", 24 | "pull-pause": "~0.0.2", 25 | "pull-ping": "^2.0.3", 26 | "pull-stream": "^3.6.14", 27 | "secret-stack-decorators": "1.1.0", 28 | "ssb-conn-db": "~1.0.5", 29 | "ssb-conn-hub": "~1.2.0", 30 | "ssb-conn-query": "~1.2.2", 31 | "ssb-conn-staging": "~1.0.0", 32 | "ssb-ref": "^2.14.3", 33 | "ssb-typescript": "^2.8.0", 34 | "statistics": "^3.3.0", 35 | "ziii": "~1.0.2" 36 | }, 37 | "peerDependencies": { 38 | "secret-stack": ">=6.2.0" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^14.14.37", 42 | "cont": "^1.0.3", 43 | "secret-stack": "^6.4.0", 44 | "ssb-caps": "~1.1.0", 45 | "ssb-db2": "~5.0.0", 46 | "ssb-lan": "~1.0.0", 47 | "ssb-server": "~16.0.1", 48 | "tap-arc": "^0.3.5", 49 | "tape": "^5.5.3", 50 | "typescript": "~4.7.4" 51 | }, 52 | "scripts": { 53 | "typescript": "tsc", 54 | "tape": "tape test/*.js | tap-arc --bail", 55 | "test": "npm run typescript && npm run tape" 56 | }, 57 | "author": "Andre Staltz (http://staltz.com)", 58 | "license": "MIT" 59 | } 60 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import {FeedId} from 'ssb-typescript'; 2 | import {CONN} from '../conn'; 3 | 4 | export type Peer = { 5 | address?: string; 6 | key?: string; 7 | host?: string; 8 | port?: number; 9 | source: 10 | | 'pub' 11 | | 'manual' 12 | | 'friends' 13 | | 'local' 14 | | 'dht' 15 | | 'bt' 16 | | 'stored'; 17 | error?: string; 18 | state?: undefined | 'connecting' | 'connected' | 'disconnecting'; 19 | stateChange?: number; 20 | failure?: number; 21 | client?: boolean; 22 | duration?: { 23 | mean: number; 24 | }; 25 | ping?: { 26 | rtt: { 27 | mean: number; 28 | }; 29 | skew: number; 30 | fail?: any; 31 | }; 32 | disconnect?: Function; 33 | }; 34 | 35 | export interface Config { 36 | conn?: { 37 | /** 38 | * Whether the CONN scheduler should start automatically as soon as the 39 | * SSB app is initialized. Default is `true`. 40 | */ 41 | autostart: boolean; 42 | 43 | /** 44 | * Whether the CONN scheduler should look into the SSB database looking for 45 | * messages of type 'pub' and add them to CONN. 46 | */ 47 | populatePubs: boolean; 48 | }; 49 | } 50 | 51 | export interface SSB { 52 | readonly id: FeedId; 53 | readonly friends?: Readonly<{ 54 | graphStream: (opts: {old: boolean; live: boolean}) => CallableFunction; 55 | hopStream: (opts: {old: boolean; live: boolean}) => CallableFunction; 56 | }>; 57 | readonly roomClient?: Readonly<{ 58 | discoveredAttendants: () => CallableFunction; 59 | }>; 60 | readonly bluetooth?: Readonly<{ 61 | nearbyScuttlebuttDevices: (x: number) => CallableFunction; 62 | }>; 63 | readonly lan?: Readonly<{ 64 | start: () => void; 65 | stop: () => void; 66 | discoveredPeers: () => CallableFunction; 67 | }>; 68 | readonly db?: Readonly<{ 69 | onMsgAdded?: (cb: CallableFunction) => CallableFunction; 70 | query: (...args: Array) => any; 71 | operators: Record; 72 | }>; 73 | readonly conn: CONN; 74 | } 75 | 76 | export type Callback = (err?: any, val?: T) => void; 77 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 6.0.0 2 | 3 | - Vastly different scheduler behavior: 4 | * New cooldown-score system to prioritize which peers to connect to 5 | * Connect to hops=`N+1` peers if no hops=`N` peers are online 6 | * Implement rotation of peers (every several minutes) to encourage diversity 7 | * If using ssb-room-client, we only support `ssb-room-client@>=2.0.0` 8 | - Remove support for `config.seeds` 9 | 10 | # 5.0.0 11 | 12 | - Scheduler dropped supports for ssb-friends lower than 5.0 13 | - Scheduler dropped support for `config.conn.hops` 14 | 15 | # 4.0.0 16 | 17 | - Change return type of `ssb.conn.dbPeers()` to array, not iterable 18 | 19 | # 3.2.1 20 | 21 | - Updated ssb-conn-db to use atomic-file-rw which should be more robust than before, causing less corrupted conn.json saves to the filesystems. 22 | 23 | # 3.2.0 24 | 25 | - Improve the scheduler: disconnect to excess peers after a long and random delay that is inversely proportional to the size of the excess. The randomization is important to avoid "back and forth dances" where remote peers connect to you, but you disconnect from them, which leads them to immediately attempt a reconnect to you, and so forth 26 | 27 | # 3.1.0 28 | 29 | - If already connected to a peer, `ssb.conn.connect()` will just return the RPC instance of that connected peer, instead of `false`. See https://github.com/staltz/ssb-conn-hub/commit/7a8a880d4abf74cc916febbe6efe441a23aed590 30 | 31 | # 3.0.0 32 | 33 | - Drop support for `ssb.gossip.*` APIs, except `ssb.gossip.ping` 34 | - If you want to support `pub` messages in the scheduler, you need `ssb-db@2` or higher 35 | - Remove the deprecated APIs `ssb.conn.internalConnDB()`, `ssb.conn.internalConnHub()`, `ssb.conn.internalConnStaging()` 36 | 37 | # 2.1.0 38 | 39 | - If already connected to a peer, `connect()` on that peer now returns the `rpc` object, not `false` 40 | 41 | # 2.0.1 42 | 43 | - Avoids global pollution of all objects with the `.z` field, something caused by the dependency `zii` in previous versions 44 | 45 | # 2.0.0 46 | 47 | - If you want to support `pub` messages in the scheduler, this version needs ssb-db2 and drops support for ssb-db. Everything else than `pub` messages will still work if you're on ssb-db. 48 | 49 | # 1.0.0 50 | 51 | Official first stable version (just a blessing of 0.19.1). 52 | 53 | # 0.18.3 54 | 55 | - Preliminary support for running in the browser -------------------------------------------------------------------------------- /test/interpool.js: -------------------------------------------------------------------------------- 1 | const tape = require('tape'); 2 | const mock = require('./mock'); 3 | 4 | const TEST_KEY = '@pAhDcHjunq6epPvYYo483vBjcuDkE10qrc2tYC827R0=.ed25519'; 5 | const TEST_ADDR = 6 | 'net:localhost:9752~shs:pAhDcHjunq6epPvYYo483vBjcuDkE10qrc2tYC827R0='; 7 | const TEST_ADDR2 = 8 | 'net:localhost:1234~shs:pAhDcHjunq6epPvYYo483vBjcuDkE10qrc2tYC827R0='; 9 | 10 | tape('CONN refuses to stage an already connected address', t => { 11 | t.plan(4); 12 | const ssb = mock(); 13 | 14 | const address = TEST_ADDR; 15 | ssb.conn.connect(address, (err, result) => { 16 | t.error(err, 'no error'); 17 | t.ok(result, 'connect was succesful'); 18 | 19 | const stagingResult = ssb.conn.stage(address, {mode: 'internet'}); 20 | t.equals(stagingResult, false, 'stage() should refuse'); 21 | 22 | const entries1 = Array.from(ssb.conn.staging().entries()); 23 | t.equals(entries1.length, 0, 'there is nothing in staging'); 24 | 25 | t.end(); 26 | }); 27 | }); 28 | 29 | tape('CONN refuses to stage an ssb key that already has a connection', t => { 30 | t.plan(4); 31 | const ssb = mock(); 32 | 33 | ssb.conn.connect(TEST_ADDR, {key: TEST_KEY}, (err, result) => { 34 | t.error(err, 'no error'); 35 | t.ok(result, 'connect was succesful'); 36 | 37 | const stagingResult = ssb.conn.stage(TEST_ADDR2, { 38 | mode: 'internet', 39 | key: TEST_KEY, 40 | }); 41 | t.equals(stagingResult, false, 'stage() should refuse'); 42 | 43 | const entries1 = Array.from(ssb.conn.staging().entries()); 44 | t.equals(entries1.length, 0, 'there is nothing in staging'); 45 | 46 | t.end(); 47 | }); 48 | }); 49 | 50 | tape('automatically unstage upon connHub "connected" event', t => { 51 | t.plan(6); 52 | const ssb = mock(); 53 | 54 | const address = TEST_ADDR; 55 | const result1 = ssb.conn.stage(address, {mode: 'internet', address}); 56 | t.equals(result1, true, 'stage() succeeds'); 57 | 58 | const entries1 = Array.from(ssb.conn.staging().entries()); 59 | t.equals(entries1.length, 1, 'there is one address in staging'); 60 | const [actualAddress] = entries1[0]; 61 | t.equals(actualAddress, TEST_ADDR, 'staged address is what we expected'); 62 | 63 | ssb.conn.connect(address, (err, result) => { 64 | t.error(err, 'no error'); 65 | t.ok(result, 'connect was succesful'); 66 | 67 | const entries2 = Array.from(ssb.conn.staging().entries()); 68 | t.equals(entries2.length, 0, 'there is nothing in staging'); 69 | 70 | t.end(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /src/interpool-glue.ts: -------------------------------------------------------------------------------- 1 | import ConnDB = require('ssb-conn-db'); 2 | import ConnHub = require('ssb-conn-hub'); 3 | import ConnStaging = require('ssb-conn-staging'); 4 | import {ListenEvent as HubEvent} from 'ssb-conn-hub/lib/types'; 5 | const pull = require('pull-stream'); 6 | const stats = require('statistics'); 7 | const ping = require('pull-ping'); 8 | 9 | export function interpoolGlue(db: ConnDB, hub: ConnHub, staging: ConnStaging) { 10 | function setupPing(address: string, rpc: any) { 11 | const PING_TIMEOUT = 5 * 6e4; // 5 minutes 12 | const pp = ping({serve: true, timeout: PING_TIMEOUT}, () => {}); 13 | db.update(address, {ping: {rtt: pp.rtt, skew: pp.skew}}); 14 | pull( 15 | pp, 16 | rpc.gossip.ping({timeout: PING_TIMEOUT}, (err: any) => { 17 | if (err?.name === 'TypeError') { 18 | db.update(address, (prev: any) => ({ 19 | ping: {...(prev.ping ?? {}), fail: true}, 20 | })); 21 | } 22 | }), 23 | pp, 24 | ); 25 | } 26 | 27 | function onConnecting(ev: HubEvent) { 28 | const address = ev.address; 29 | const stagedData = staging.get(address); 30 | staging.unstage(address); 31 | for (const [addr, data] of staging.entries()) { 32 | if (data.key && data.key === ev.key) staging.unstage(addr); 33 | } 34 | db.update(address, {stateChange: Date.now()}); 35 | const dbData = db.get(address); 36 | hub.update(address, {...dbData, ...stagedData}); 37 | } 38 | 39 | function onConnectingFailed(ev: HubEvent) { 40 | db.update(ev.address, (prev: any) => ({ 41 | failure: (prev.failure ?? 0) + 1, 42 | stateChange: Date.now(), 43 | duration: stats(prev.duration, 0), 44 | })); 45 | } 46 | 47 | function onConnected(ev: HubEvent) { 48 | const address = ev.address; 49 | const stagedData = staging.get(address); 50 | staging.unstage(address); 51 | for (const [addr, data] of staging.entries()) { 52 | if (data.key && data.key === ev.key) staging.unstage(addr); 53 | } 54 | db.update(address, {stateChange: Date.now(), failure: 0}); 55 | const dbData = db.get(address); 56 | hub.update(address, {...dbData, ...stagedData}); 57 | if (ev.details.isClient) setupPing(address, ev.details.rpc); 58 | } 59 | 60 | function onDisconnecting(ev: HubEvent) { 61 | db.update(ev.address, {stateChange: Date.now()}); 62 | } 63 | 64 | function onDisconnectingFailed(ev: HubEvent) { 65 | db.update(ev.address, {stateChange: Date.now()}); 66 | } 67 | 68 | function onDisconnected(ev: HubEvent) { 69 | db.update(ev.address, (prev: any) => ({ 70 | stateChange: Date.now(), 71 | duration: stats(prev.duration, Date.now() - prev.stateChange), 72 | })); 73 | // TODO ping this address to see if it's worth re-staging it 74 | // But how to "ping" without multiserver-connecting to them? 75 | } 76 | 77 | pull( 78 | hub.listen(), 79 | pull.drain((ev: HubEvent) => { 80 | if (ev.type === 'connecting') onConnecting(ev); 81 | if (ev.type === 'connecting-failed') onConnectingFailed(ev); 82 | if (ev.type === 'connected') onConnected(ev); 83 | if (ev.type === 'disconnecting') onDisconnecting(ev); 84 | if (ev.type === 'disconnecting-failed') onDisconnectingFailed(ev); 85 | if (ev.type === 'disconnected') onDisconnected(ev); 86 | }), 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/conn.ts: -------------------------------------------------------------------------------- 1 | import ConnDB = require('ssb-conn-db'); 2 | import ConnHub = require('ssb-conn-hub'); 3 | import ConnStaging = require('ssb-conn-staging'); 4 | import ConnQuery = require('ssb-conn-query'); 5 | import {StagedData} from 'ssb-conn-staging/lib/types'; 6 | import {plugin, muxrpc} from 'secret-stack-decorators'; 7 | import {Callback} from './types'; 8 | import {interpoolGlue} from './interpool-glue'; 9 | const ping = require('pull-ping'); 10 | 11 | @plugin('1.0.0') 12 | export class CONN { 13 | private readonly ssb: any; 14 | private readonly config: any; 15 | private readonly _db: ConnDB; 16 | private readonly _hub: ConnHub; 17 | private readonly _staging: ConnStaging; 18 | private readonly _query: ConnQuery; 19 | 20 | constructor(ssb: any, cfg: any) { 21 | this.ssb = ssb; 22 | this.config = cfg; 23 | this._db = new ConnDB({path: this.config.path, writeTimeout: 1e3}); 24 | this._hub = new ConnHub(this.ssb); 25 | this._staging = new ConnStaging(); 26 | this._query = new ConnQuery(this._db, this._hub, this._staging); 27 | 28 | this.initialize(); 29 | } 30 | 31 | //#region Initialization 32 | 33 | private initialize() { 34 | this.setupCloseHook(); 35 | this.maybeAutoStartScheduler(); 36 | interpoolGlue(this._db, this._hub, this._staging); 37 | } 38 | 39 | private setupCloseHook() { 40 | const that = this; 41 | this.ssb.close.hook(function (this: any, fn: Function, args: Array) { 42 | that.stopScheduler(); 43 | that._hub.close(); 44 | that._staging.close(); 45 | that._db.close(); // Has to happen last, because Hub / Staging write to it 46 | return fn.apply(this, args); 47 | }); 48 | } 49 | 50 | private maybeAutoStartScheduler() { 51 | if (this.config.conn?.autostart === false) { 52 | // opt-out from starting the scheduler 53 | } else { 54 | // by default, start the scheduler 55 | this.startScheduler(); 56 | } 57 | } 58 | 59 | //#endregion 60 | 61 | //#region Helper methods 62 | 63 | private async startScheduler() { 64 | await this._db.loaded(); 65 | 66 | if (this.ssb.connScheduler) { 67 | this.ssb.connScheduler.start(); 68 | } else { 69 | // Maybe this is a race condition, so let's wait a bit more 70 | setTimeout(() => { 71 | if (this.ssb.connScheduler) { 72 | this.ssb.connScheduler.start(); 73 | } else { 74 | console.error( 75 | 'There is no ConnScheduler! ' + 76 | 'The CONN plugin will remain in manual mode.', 77 | ); 78 | } 79 | }, 100); 80 | } 81 | } 82 | 83 | private stopScheduler() { 84 | if (this.ssb.connScheduler) this.ssb.connScheduler.stop(); 85 | } 86 | 87 | //#endregion 88 | 89 | //#region PUBLIC MUXRPC 90 | 91 | @muxrpc('sync') 92 | public remember = (address: string, data: any = {}) => { 93 | this._db.set(address, data); 94 | }; 95 | 96 | @muxrpc('sync') 97 | public forget = (address: string) => { 98 | this._db.delete(address); 99 | }; 100 | 101 | @muxrpc('sync') 102 | public dbPeers = () => [...this._db.entries()]; 103 | 104 | @muxrpc('async') 105 | public connect = ( 106 | address: string, 107 | b?: Record | null | undefined | Callback, 108 | c?: Callback, 109 | ) => { 110 | if (c && (typeof b === 'function' || !b)) { 111 | throw new Error('CONN.connect() received incorrect arguments'); 112 | } 113 | const last = !!c ? c : b; 114 | const cb = (typeof last === 'function' ? last : null) as Callback; 115 | const data = (typeof b === 'object' ? b : {}) as any; 116 | 117 | this._hub.connect(address, data).then( 118 | (result) => cb?.(null, result), 119 | (err) => cb?.(err), 120 | ); 121 | }; 122 | 123 | @muxrpc('async') 124 | public disconnect = (address: string, cb?: Callback) => { 125 | this._hub.disconnect(address).then( 126 | (result) => cb?.(null, result), 127 | (err) => cb?.(err), 128 | ); 129 | }; 130 | 131 | @muxrpc('source') 132 | public peers = () => this._hub.liveEntries(); 133 | 134 | @muxrpc('sync') 135 | public stage = ( 136 | address: string, 137 | data: Partial = {type: 'internet'}, 138 | ) => { 139 | if (!!this._hub.getState(address)) return false; 140 | if (data.key) { 141 | for (const other of this._hub.entries()) { 142 | if (other[1].key === data.key) return false; 143 | } 144 | } 145 | 146 | return this._staging.stage(address, data); 147 | }; 148 | 149 | @muxrpc('sync') 150 | public unstage = (address: string) => { 151 | return this._staging.unstage(address); 152 | }; 153 | 154 | @muxrpc('source') 155 | public stagedPeers = () => this._staging.liveEntries(); 156 | 157 | @muxrpc('sync') 158 | public start = () => { 159 | return this.startScheduler(); 160 | }; 161 | 162 | @muxrpc('sync') 163 | public stop = () => { 164 | this.stopScheduler(); 165 | }; 166 | 167 | @muxrpc('duplex', {anonymous: 'allow'}) 168 | public ping = () => { 169 | const MIN = 10e3; 170 | const DEFAULT = 5 * 60e3; 171 | const MAX = 30 * 60e3; 172 | let timeout = this.config.timers?.ping ?? DEFAULT; 173 | timeout = Math.max(MIN, Math.min(timeout, MAX)); 174 | return ping({timeout}); 175 | }; 176 | 177 | @muxrpc('sync') 178 | public db = () => this._db; 179 | 180 | @muxrpc('sync') 181 | public hub = () => this._hub; 182 | 183 | @muxrpc('sync') 184 | public staging = () => this._staging; 185 | 186 | @muxrpc('sync') 187 | public query = () => this._query; 188 | 189 | //#endregion 190 | } 191 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # SSB CONN 4 | 5 | ### SSB plugin for establishing and managing peer connections. 6 | 7 |
8 | 9 | *CONN* (Connections Over Numerous Networks) plugin replaces the old `gossip` plugin, covering all its use cases. CONN has these responsibilities: 10 | 11 | - Persistence of pub (and other servers) addresses (in the file `~/.ssb/conn.json`) 12 | - Monitoring of all current connections and their state (connecting, disconnecting, etc) 13 | - Monitoring of discovered peers and suggested connections (e.g. on LAN or Bluetooth) 14 | - Selection and scheduling of connections and disconnections 15 | - API compatibility with the old gossip plugin 16 | 17 | 18 | ## Installation 19 | 20 | **Prerequisites:** 21 | 22 | - Requires **Node.js 6.5** or higher 23 | - Requires `secret-stack@^6.2.0` 24 | 25 | **Recommended:** 26 | 27 | Not required, but: 28 | 29 | - The default scheduler in `ssb-conn@>=2.0.0` wants to use `ssb-db2@>=1.18.0` and `ssb-friends@>=4.4.4` 30 | - The default scheduler in (older) `ssb-conn@1.0.0` wants to use `ssb-db@>=19` and `ssb-friends` 31 | 32 | ``` 33 | npm install --save ssb-conn 34 | ``` 35 | 36 | Add this plugin to ssb-server like this: 37 | 38 | ```diff 39 | var createSsbServer = require('ssb-server') 40 | .use(require('ssb-onion')) 41 | .use(require('ssb-unix-socket')) 42 | .use(require('ssb-no-auth')) 43 | .use(require('ssb-master')) 44 | .use(require('ssb-db2')) 45 | + .use(require('ssb-conn')) 46 | .use(require('ssb-replicate')) 47 | .use(require('ssb-friends')) 48 | // ... 49 | ``` 50 | 51 | Now you should be able to access the muxrpc APIs under `ssb.conn` and `ssb.gossip`, see next section. 52 | 53 | ## Basic API 54 | 55 | You can call any of these APIs in your local peer. 56 | 57 | This uses [multiserver](https://github.com/ssb-js/multiserver) addresses. 58 | 59 | | API | Type | Description | 60 | |-----|------|-------------| 61 | | **`ssb.conn.connect(addr, data?, cb)`** | `async` | Connects to a peer known by its multiserver address `addr`, and stores additional optional `data` (as an object) during its connection lifespan. | 62 | | **`ssb.conn.disconnect(addr, cb)`** | `async` | Disconnects a peer known by its multiserver address `addr`. | 63 | | **`ssb.conn.peers()`** | `source` | A pull-stream that emits an array of all ConnHub entries whenever any connection updates (i.e. changes it state: connecting, disconnecting, connected, etc). | 64 | | **`ssb.conn.remember(addr, data?)`** | `sync` | Stores (in cold storage) connection information about a new peer, known by its multiserver address `addr` and additional optional `data` (as an object). | 65 | | **`ssb.conn.forget(addr)`** | `sync` | Removes (from cold storage) connection information about a peer known by its multiserver address `addr`. | 66 | | **`ssb.conn.dbPeers()`** | `sync` | Returns an array of ConnDB entries known at the moment. Does not reactively update once the database is written to. | 67 | | **`ssb.conn.stage(addr, data?)`** | `sync` | Registers a suggested connection to a new peer, known by its multiserver address `addr` and additional optional `data` (as an object). | 68 | | **`ssb.conn.unstage(addr)`** | `sync` | Unregisters a suggested connection the peer known by its multiserver address `addr`. | 69 | | **`ssb.conn.stagedPeers()`** | `source` | A pull-stream that emits an array of all ConnStaging entries whenever any staging status updates (upon stage() or unstage()). | 70 | | **`ssb.conn.start()`** | `sync` | Triggers the start of the connections scheduler in CONN. | 71 | | **`ssb.conn.stop()`** | `sync` | Stops the scheduler if it is currently active. | 72 | 73 | An "entry" is a (tuple) array of form: 74 | 75 | ```javascript 76 | [addr, data] 77 | ``` 78 | where: 79 | - `addr` is a multiserver address (a **string** that [follows some rules](https://github.com/dominictarr/multiserver-address)) 80 | - `data` is an **object** with additional information about the peer 81 | 82 |
83 | Full details on fields present in data (click here) 84 | 85 | Fields marked 🔷 are important and often used, fields marked 🔹 come from CONN, fields marked 🔸 are ad-hoc and added by various other modules, and fields suffixed with `?` are not always present: 86 | 87 | 🔷 `key: string`: the peer's public key / feedId 88 | 89 | 🔷 `state?: 'connecting' | 'connected' | 'disconnecting'`: (only from `peers()`) the peer's current connection status 90 | 91 | 🔷 `type?: string`: what type of peer this is; it can be any string, but often is either `'lan'`, `'bt'`, `'pub'`, `'room'`, `'room-endpoint'`, `'dht'` 92 | 93 | 🔹 `inferredType?: 'bt' | 'lan' | 'dht' | 'internet' | 'tunnel'`: (only from `peers()`) when there is no `type` field, e.g. when a new and unknown peer initiates a client connection with us (as a server), then ConnHub makes a guess what type it is 94 | 95 | 🔹 `birth?: number`: Unix timestamp for when this peer was added to ConnDB 96 | 97 | 🔹 `stateChange?: number`: Unix timestamp for the last time the field `state` was changed; this is stored in ConnDB 98 | 99 | 🔹 `hubBirth?: number`: Unix timestamp for when this peer was added to ConnHub 100 | 101 | 🔹 `hubUpdated?: number`: Unix timestamp for when this data object was last updated in ConnHub, which means the last time it was connected or attempted 102 | 103 | 🔹 `stagingBirth?: number`: Unix timestamp for when this peer was added to ConnStaging 104 | 105 | 🔹 `stagingUpdated?: number`: Unix timestamp for when this data object was last updated in ConnStaging 106 | 107 | 🔹 `autoconnect?: boolean`: indicates whether this peer should be considered for automatic connection in the scheduler. By the default this field is considered `true` whenever it's undefined, and if you want opt-out of automatic connections for this peer (thus delegating it to a manual choice by the user), then set it to `false`. 108 | 109 | 🔹 `failure?: number`: typically stored in ConnDB, this is the number of connection errors since the last successful connection 110 | 111 | 🔹 `duration?: object`: typically stored in ConnDB, this is a [statistics](https://www.npmjs.com/package/statistics) object to measure the duration of connection with this peer 112 | 113 | 🔹 `ping?: object`: typically stored in ConnDB, this is [statistics](https://www.npmjs.com/package/statistics) object of various ping health measurements 114 | 115 | 🔹 `pool?: 'db' | 'hub' | 'staging'`: this only appears in ConnQuery APIs, and indicates from which pool (ConnDB or ConnHub or ConnStaging) was this peer picked 116 | 117 | 🔸 `name?: string`: a nickname for this peer, when there isn't an [ssb-about](https://github.com/ssbc/ssb-about) name 118 | 119 | 🔸 `room?: string`: (only if `type = 'room-attendant'`) the public key of the [room](https://github.com/staltz/ssb-room) server where this peer is in 120 | 121 | 🔸 `onlineCount?: number`: (only if `type = 'room'`) the number of room endpoints currently connected to this room 122 | 123 |
124 | 125 | ## Advanced API 126 | 127 | CONN also provides more detailed APIs by giving you access to the internals, ConnDB, ConnHub, ConnStaging, ConnQuery. These are APIs that we discourage using, simply because in the vast majority of the cases, the basic API is enough (you might just need a few pull-stream operators on the basic APIs), but if you know what you're doing, don't feel afraid to use the advanced APIs! 128 | 129 | | API | Type | Description | 130 | |-----|------|-------------| 131 | | **`ssb.conn.ping()`** | `duplex` | A duplex pull-stream for periodically pinging with peers, fully compatible with `ssb.gossip.ping`. | 132 | | **`sbb.conn.db()`** | `sync` | Returns the instance of [ConnDB](https://github.com/staltz/ssb-conn-db) currently in use. Read their docs to get access to more APIs. | 133 | | **`ssb.conn.hub()`** | `sync` | Returns the instance of [ConnHub](https://github.com/staltz/ssb-conn-hub) currently in use. Read their docs to get access to more APIs. | 134 | | **`ssb.conn.staging()`** | `sync` | Returns the instance of [ConnStaging](https://github.com/staltz/ssb-conn-staging) currently in use. Read their docs to get access to more APIs. | 135 | | **`ssb.conn.query()`** | `sync` | Returns the instance of [ConnQuery](https://github.com/staltz/ssb-conn-query) currently in use. Read their docs to get access to more APIs. | 136 | 137 | 138 | ## (Deprecated) Gossip API 139 | 140 | The following gossip plugin APIs are available once you install CONN: 141 | 142 | | API | Type | 143 | |-----|------| 144 | | **`ssb.gossip.ping()`** | `duplex` | 145 | 146 | If you want to use other legacy `ssb.gossip.*` APIs and preserve the same gossip behavior as before, use [`ssb-legacy-conn`](https://github.com/staltz/ssb-legacy-conn) which uses parts of CONN and tries to mirrors the old gossip plugin as closely as possible, even its log messages. 147 | 148 | ## Config 149 | 150 | Some parameters in CONN can be configured by the user or by application code through the conventional [ssb-config](https://github.com/ssbc/ssb-config). The possible options are listed below: 151 | 152 | ```typescript 153 | { 154 | "conn": { 155 | /** 156 | * Whether the CONN scheduler should start automatically as soon as the 157 | * SSB app is initialized. Default is `true`. 158 | */ 159 | "autostart": boolean, 160 | 161 | /** 162 | * Whether the CONN scheduler should look into the SSB database looking for 163 | * messages of type 'pub' and add them to CONN. Default is `true`. 164 | */ 165 | "populatePubs": boolean, 166 | } 167 | } 168 | ``` 169 | 170 | ## Recipes 171 | 172 |
173 | How can I get a pull stream of all currently connected peers? (click here) 174 |

175 | 176 | You can use `ssb.conn.peers()` to get a stream of "all peers currently being processed" and then use Array `filter` to pick only peers that are strictly *connected*, ignoring those that are *connecting* or *disconnecting*: 177 | 178 | ```js 179 | var connectedPeersStream = pull( 180 | ssb.conn.peers(), 181 | pull.map(entries => 182 | entries.filter(([addr, data]) => data.state === 'connected') 183 | ) 184 | ) 185 | ``` 186 | 187 | Then you can drain the stream to get an array of connected peers: 188 | 189 | ```js 190 | pull( 191 | connectedPeersStream, 192 | pull.drain(connectedPeers => { 193 | console.log(connectedPeers) 194 | // [ 195 | // ['net:192.168.1...', {key: '@Ql...', ...}], 196 | // ['net:192.168.2...', {key: '@ye...', ...}] 197 | // ] 198 | }) 199 | ) 200 | ``` 201 | 202 |

    203 | 204 |

    205 |
    206 | 207 | 208 |
    209 | How can I immediately get all currently connected peers? (click here) 210 |

    211 | 212 | [ssb-conn-query](https://github.com/staltz/ssb-conn-query) has APIs for that and others, e.g. 213 | 214 | ```js 215 | var arr = ssb.conn.query().peersConnected() 216 | 217 | console.log(arr) 218 | // [ 219 | // ['net:192.168.1...', {key: '@Ql...', ...}], 220 | // ['net:192.168.2...', {key: '@ye...', ...}] 221 | // ] 222 | ``` 223 | 224 | If the above doesn't work (for instance, `conn.query()` is not available in the CLI and other similar cases), you can use `ssb.conn.peers()` plus some pull-stream operators: 225 | 226 | ```js 227 | function getConnectedPeersNow(cb) { 228 | pull( 229 | ssb.conn.peers(), 230 | pull.map(entries => 231 | entries.filter(([addr, data]) => data.state === 'connected') 232 | ) 233 | pull.take(1), // This is important 234 | pull.drain(connectedPeers => cb(null, connectedPeers)) 235 | ) 236 | } 237 | 238 | getConnectedPeersNow(arr => console.log(arr)) 239 | ``` 240 | 241 |

      242 | 243 |

      244 |
      245 | 246 | ## Learn more 247 | 248 |
      249 | How CONN works (click here) 250 |

      251 | 252 | ![diagram.png](diagram.png) 253 | 254 | Under the hood, CONN is based on three "pools" of peers: 255 | 256 | - [ConnDB](https://github.com/staltz/ssb-conn-db): a persistent database of addresses to connect to 257 | - [ConnHub](https://github.com/staltz/ssb-conn-hub): a façade API for currently active connections 258 | - [ConnStaging](https://github.com/staltz/ssb-conn-staging): a pool of potential new connections 259 | 260 | ConnDB contains metadata on stable servers and peers that have been successfully connectable. ConnHub is the central API that allows us to issue new connections and disconnections, as well as to track the currently active connections. ConnStaging is an in-memory ephemeral storage of new possible connections that the user might want to approve or disapprove. 261 | 262 | Then, [ConnQuery](https://github.com/staltz/ssb-conn-query) has access to those three pools, and provides utilities to query, filter, and sort connections across all those pools. 263 | 264 | **ConnScheduler** is an **opinionated** (⚠️) plugin that utilizes ConnQuery to select peers to connect to, then schedules connections to happen via ConnHub, as well as schedules disconnections if necessary. Being opinionated, CONN provides an easy way of replacing the default scheduler with your own scheduler, see instructions below. 265 | 266 | There is also a **Gossip Compatibility** plugin, implementing all the legacy APIs, so that other SSB plugins that call these APIs will continue to function as normal. 267 | 268 | When you install the ssb-plugin, it will actually setup three plugins: 269 | 270 | ``` 271 | [conn, connScheduler, gossip] 272 | ``` 273 | 274 |

      275 |
      276 | 277 |
      278 | Opinions built into the default scheduler (click here) 279 |

      280 | 281 | The default scheduler is roughly the same as the legacy ssb-gossip plugin, with some opinions removed and others added. The scheduler has two parts: discovery setup on startup, and periodic connections/disconnections. 282 | 283 | **Discovery setup:** 284 | 285 | - Read the SSB log and look for "pub" messages, and `remember` them 286 | - Listen to a stream of LAN peers (see [ssb-lan](https://github.com/staltz/ssb-lan)), and `stage` them 287 | - Listen to a stream of Bluetooth nearby devices, and `stage` them 288 | - Listen to a stream of peers online in Rooms, and `stage` them 289 | 290 | **Periodic connections/disconnections:** 291 | 292 | - Try to maintain connections with 5 room servers 293 | - If we're connected to more than 5, then after some minutes we'll start disconnecting from some rooms 294 | - Try to maintain connections with 4 non-room peers (pubs, room attendants, LAN peers, etc) 295 | - If we're connected to more than 4, then after some minutes we'll start disconnecting from some 296 | - The lower the hops distance of the peer, the higher priority they receive 297 | - The more connection failures the peer has presented, the lower the priority 298 | - Room attendants and LAN peers have slight priority over pubs 299 | - After we've been connected to a peer for many minutes, disconnect from them 300 | and try to connect to different peers, to encourage diversity of connections 301 | 302 | In none of the cases above shall we connect to a peer that we block. In addition to the above, the following actions happen automatically every (approximately) 1 second: 303 | 304 | - Disconnect from connected peers that have just been blocked by us 305 | - Disconnect from peers that have been connected with us for more than 30min 306 | - Disconnect from peers that have been pending in "connecting" status for too long 307 | - "Too long" means 30sec for LAN peers 308 | - "Too long" means 30sec for Room attendants 309 | - "Too long" means 1min for Bluetooth peers 310 | - "Too long" means 5min for DHT invite peers 311 | - For other types of peers, "too long" means 10sec 312 | - Stage non-blocked peers that are in ConnDB marked as `autoconnect=false` 313 | - Unstage peers that have just been blocked by us 314 | - Unstage LAN peers that haven't been updated in ConnStaging in 10 seconds 315 | - Unstage Bluetooth peers that haven't been updated in ConnStaging in 30 seconds 316 | 317 | **Database cleanups:** 318 | 319 | Upon starting the scheduler: 320 | 321 | - Remove database entries for any LAN or Bluetooth peers (these are rediscovered just-in-time) 322 | - Remove room alias addresses if those aliases are in rooms where I have membership 323 | 324 | **Other events:** 325 | 326 | - Upon wakeup (from computer 'sleep'), fully reset the ConnHub 327 | - Upon network (interface) changes, fully reset the ConnHub 328 | - Upon a disconnection, try to connect to some peer (section above) 329 | 330 |

        331 | 332 |

        333 |
        334 | 335 |
        336 | How to build your own ConnScheduler (click here) 337 |

        338 | 339 | To experiment with your own opinions for establishing connections, you can make your own ConnScheduler, which is just a typical SSB plugin. You can write in the traditional style (like other SSB plugins), or with OOP decorators. The example below uses OOP decorators. 340 | 341 | Here is the basic shape of the scheduler: 342 | 343 | ```javascript 344 | module.exports = { 345 | name: 'connScheduler', 346 | version: '1.0.0', 347 | manifest: { 348 | start: 'sync', 349 | stop: 'stop', 350 | }, 351 | init(ssb, config) { 352 | return { 353 | start() { 354 | // this is called when the scheduler should begin making connections 355 | 356 | // You have access to CONN core here: 357 | ssb.conn.stage('some multiserver address'); 358 | ssb.conn.disconnect('another multiserver address'); 359 | // ... 360 | }, 361 | 362 | stop() { 363 | // this is called when the scheduler should cancel its jobs, if any 364 | } 365 | } 366 | } 367 | } 368 | ``` 369 | 370 | Note that the name of the plugin must be **exactly `connScheduler`** (or `connScheduler`) and it **must have the methods start() and stop()**, because the CONN core will try to use your scheduler under those names. The rest of the contents of the ConnScheduler class are up to you, you can use private methods, etc. 371 | 372 | When you're done building your scheduler, you can export it together with CONN core and the gossip compatibility plugin like this: 373 | 374 | ```js 375 | var CONN = require('ssb-conn/core') 376 | var Gossip = require('ssb-conn/compat') 377 | var ConnScheduler = require('./my-scheduler') 378 | 379 | module.exports = [CONN, ConnScheduler, Gossip] 380 | ``` 381 | 382 | That array is a valid secret-stack plugin which you can `.use()` in ssb-server. 383 | 384 |

          385 | 386 |

          387 |
          388 | 389 |
          390 | Why was the gossip plugin refactored? (click here) 391 |

          392 | 393 | The legacy gossip plugin is one of the oldest parts of the SSB stack in Node.js, and it contained several old opinions. It wasn't designed with multiserver in mind, so it made a lot of assumptions that peers have `host`/`port` fields. Nowadays with Bluetooth and other unusual modes of connectivity, that assumption breaks down often. 394 | 395 | The gossip plugin also did not have the concept of "staging", which is useful for ephemeral connections (LAN or Bluetooth) in spaces that may have many strangers. So the gossip plugin tended to connect as soon as possible to any peer discovered. 396 | 397 | Also, since the gossip plugin was a monolith, it had all these concerns (cold persistence, in-memory tracking of current connections, ephemeral peers, scheduling, old and new style addresses) squashed into one file, making it hard and brittle to change the code. 398 | 399 | The objectives with CONN were to: 400 | 401 | - Untangle the codebase into modular components with single responsibilities 402 | - Standardize the assumption that addresses are always multiserver addresses 403 | - All "pools" (DB, Hub, Staging) are key-value pairs `[address, dataObject]` 404 | - Make scheduling logic easily swappable but provide an opinionated default 405 | 406 |

            407 | 408 |

            409 |
            410 | 411 | ## License 412 | 413 | MIT 414 | -------------------------------------------------------------------------------- /src/conn-scheduler.ts: -------------------------------------------------------------------------------- 1 | import z = require('ziii'); 2 | import {Msg, FeedId} from 'ssb-typescript'; 3 | import {plugin, muxrpc} from 'secret-stack-decorators'; 4 | import {AddressData as DBData} from 'ssb-conn-db/lib/types'; 5 | import {ListenEvent as HubEvent} from 'ssb-conn-hub/lib/types'; 6 | import {StagedData} from 'ssb-conn-staging/lib/types'; 7 | import {Discovery as LANDiscovery} from 'ssb-lan/lib/types'; 8 | import {Peer} from 'ssb-conn-query/lib/types'; 9 | import ConnQuery = require('ssb-conn-query'); 10 | const {hasNoAttempts, hasOnlyFailedAttempts} = ConnQuery; 11 | const pull = require('pull-stream'); 12 | const Pausable = require('pull-pause'); 13 | const ip = require('ip'); 14 | const onWakeup = require('on-wakeup'); 15 | const onNetwork = require('on-change-network-strict'); 16 | const hasNetworkRightNow = require('has-network2'); 17 | const Ref = require('ssb-ref'); 18 | const debug = require('debug')('ssb:conn:scheduler'); 19 | import {Config, SSB} from './types'; 20 | 21 | const SECONDS = 1e3; 22 | const MINUTES = 60e3; 23 | const HOUR = 60 * 60e3; 24 | 25 | /** 26 | * A random number between 0.8 and 1.2 to add some randomization, but a fixed 27 | * number for the current runtime. This is just to make sure that other peers 28 | * don't have the exact same wait periods as us, so to avoid deadlocks. 29 | */ 30 | const RANDOM_MULTIPLIER = 0.8 + Math.random() * 0.4; 31 | 32 | let lastCheck = 0; 33 | let lastValue: any = null; 34 | function hasNetwork() { 35 | if (lastCheck + 1e3 < Date.now()) { 36 | lastCheck = Date.now(); 37 | lastValue = hasNetworkRightNow(); 38 | } 39 | return lastValue; 40 | } 41 | 42 | function take(n: number) { 43 | return (arr: Array) => arr.slice(0, Math.max(n, 0)); 44 | } 45 | 46 | function filter(condition: (peer: Peer) => boolean) { 47 | return (arr: Array) => arr.filter(condition); 48 | } 49 | 50 | type Type = 51 | | 'bt' 52 | | 'lan' 53 | | 'internet' 54 | | 'dht' 55 | | 'pub' 56 | | 'room' 57 | | 'room-attendant-alias' 58 | | 'room-attendant' 59 | | '?'; 60 | 61 | function detectType(peer: Peer): Type { 62 | const [addr, data] = peer; 63 | if (data.type === 'bt') return 'bt'; 64 | if (data.type === 'lan') return 'lan'; 65 | if (data.type === 'internet') return 'internet'; 66 | if (data.type === 'dht') return 'dht'; 67 | if (data.type === 'pub') return 'pub'; 68 | if (data.type === 'room') return 'room'; 69 | if (data.type === 'room-endpoint' || data.type === 'room-attendant') { 70 | if (data.alias) return 'room-attendant-alias'; 71 | else return 'room-attendant'; // legacy 72 | } 73 | if (data.source === 'local') return 'lan'; 74 | if (data.source === 'pub') return 'pub'; 75 | if (data.source === 'internet') return 'internet'; 76 | if (data.source === 'dht') return 'dht'; 77 | if (data.inferredType === 'bt') return 'bt'; 78 | if (data.inferredType === 'lan') return 'lan'; 79 | if (data.inferredType === 'dht') return 'dht'; 80 | if (data.inferredType === 'internet') return 'internet'; 81 | if (addr.startsWith('bt:')) return 'bt'; 82 | if (addr.startsWith('dht:')) return 'dht'; 83 | return '?'; 84 | } 85 | 86 | const isNotLocalhost = (p: Peer) => 87 | !ip.isLoopback(p[1].host) && p[1].host !== 'localhost'; 88 | 89 | function isNotRoom(peer: Peer): boolean { 90 | return peer[1].type !== 'room'; 91 | } 92 | 93 | function isRoom(peer: Peer): boolean { 94 | return peer[1].type === 'room'; 95 | } 96 | 97 | function isDefunct(peer: Peer | [string, DBData]): boolean { 98 | return peer[1].defunct === true; 99 | } 100 | 101 | /** 102 | * Given an excess of connected peers, pick the ones that have been connected 103 | * long enough. "Long enough" is 2minutes divided by the excess, so that the 104 | * more excess we have, the quicker we trigger disconnections. The less excess, 105 | * the longer we wait to trigger a disconnection. 106 | */ 107 | function filterOldExcess(excess: number) { 108 | return (peers: Array) => { 109 | const WAIT_TIME = 2 * MINUTES * RANDOM_MULTIPLIER; 110 | return peers.filter( 111 | (p) => Date.now() > p[1].hubUpdated! + WAIT_TIME / excess, 112 | ); 113 | }; 114 | } 115 | 116 | function sortByOldestConnection(peers: Array) { 117 | return peers.sort((a, b) => { 118 | return a[1].hubUpdated! - b[1].hubUpdated!; 119 | }); 120 | } 121 | 122 | function calculateCooldown( 123 | fullPercent: number, 124 | hops: Record, 125 | ) { 126 | return (peers: Array) => { 127 | return peers.map((peer) => { 128 | const [, data] = peer; 129 | const peerType = detectType(peer); 130 | 131 | // 10% is considered the smallest measurement of full 132 | const normalizedFullPercent = Math.max(0.1, fullPercent); 133 | 134 | // The larger the hop, the longer the cooldown. Handle special cases too 135 | const hop = hops[data.key!]; 136 | const hopsCooldown = 137 | peerType === 'room' // room is always considered at a constant distance 138 | ? 1 * SECONDS 139 | : hop === null || hop === void 0 || hop < 0 140 | ? Infinity 141 | : hop * SECONDS; 142 | 143 | // The more connection failures happened, the longer the cooldown is 144 | const retryCooldown = 145 | 4 * SECONDS + Math.min(64, data.failure || 0) ** 3 * 10 * SECONDS; 146 | 147 | // Sum the two together 148 | let cooldown = (hopsCooldown + retryCooldown) * normalizedFullPercent; 149 | 150 | // Make the cooldown shorter if the peer is new 151 | if (hasNoAttempts(peer)) cooldown *= 0.5; 152 | // Make the cooldown shorter randomly sometimes, to encourage exploration 153 | if (Math.random() < 0.3) cooldown *= 0.5; 154 | // Make the cooldown shorter for some special types of peers: 155 | if (peerType === 'lan') cooldown *= 0.7; 156 | if (peerType === 'room-attendant') cooldown *= 0.8; 157 | // Make the cooldown longer if the peer has problems 158 | if (hasOnlyFailedAttempts(peer)) cooldown *= 3; 159 | 160 | data.cooldown = cooldown; 161 | return peer; 162 | }); 163 | }; 164 | } 165 | 166 | function cooledDownEnough(peer: Peer) { 167 | const [, data] = peer; 168 | const lastAttempt = data.stateChange ?? data.hubUpdated ?? 0; 169 | if (data.cooldown === undefined) return true; 170 | return Date.now() > lastAttempt + data.cooldown; 171 | } 172 | 173 | function sortByCooldownAscending(peers: Array) { 174 | return peers.sort((a, b) => { 175 | const [, aData] = a; 176 | const [, bData] = b; 177 | if (aData.cooldown === undefined) return 1; 178 | if (bData.cooldown === undefined) return -1; 179 | return aData.cooldown! - bData.cooldown!; 180 | }); 181 | } 182 | 183 | @plugin('1.0.0') 184 | export class ConnScheduler { 185 | private readonly ssb: SSB; 186 | private readonly config: Config; 187 | private pubDiscoveryPausable?: { 188 | pause: CallableFunction; 189 | resume: CallableFunction; 190 | }; 191 | private intervalForUpdate?: NodeJS.Timeout; 192 | private ssbDB2Subscription?: CallableFunction | undefined; 193 | private closed: boolean; 194 | private loadedHops: boolean; 195 | private lastMessageAt: number; 196 | private lastRotationAt: number; 197 | private hasScheduledAnUpdate: boolean; 198 | private hops: Record; 199 | 200 | constructor(ssb: any, config: any) { 201 | this.ssb = ssb; 202 | this.config = config; 203 | this.closed = true; 204 | this.lastMessageAt = 0; 205 | this.lastRotationAt = 0; 206 | this.hasScheduledAnUpdate = false; 207 | this.loadedHops = false; 208 | this.hops = {}; 209 | } 210 | 211 | private loadSocialGraph() { 212 | if (!this.ssb.friends?.hopStream) { 213 | debug('Warning: ssb-friends@5 is missing, scheduling is degraded'); 214 | this.loadedHops = true; 215 | return; 216 | } 217 | 218 | pull( 219 | this.ssb.friends?.hopStream({live: true, old: true}), 220 | pull.drain((h: Record) => { 221 | this.hops = {...this.hops, ...h}; 222 | this.loadedHops = true; 223 | }), 224 | ); 225 | } 226 | 227 | private isCurrentlyDownloading() { 228 | // don't schedule new connections if currently downloading messages 229 | return this.lastMessageAt && this.lastMessageAt > Date.now() - 500; 230 | } 231 | 232 | private isBlocked = (peer: [Peer[0], Pick]) => { 233 | const [, data] = peer; 234 | if (!data?.key) return false; 235 | return this.hops[data.key] === -1; 236 | }; 237 | 238 | private isNotBlocked = (peer: [Peer[0], Pick]) => { 239 | return !this.isBlocked(peer); 240 | }; 241 | 242 | private isNotConnected = (address: string) => { 243 | return !this.ssb.conn.hub().getState(address); 244 | }; 245 | 246 | private maxWaitToConnect(peer: Peer): number { 247 | const type = detectType(peer); 248 | switch (type) { 249 | case 'lan': 250 | return 30e3; 251 | 252 | case 'room-attendant-alias': 253 | return 30e3; 254 | 255 | case 'bt': 256 | return 60e3; 257 | 258 | case 'dht': 259 | return 300e3; 260 | 261 | default: 262 | return 10e3; 263 | } 264 | } 265 | 266 | // Utility to connect to bunch of peers, or disconnect if over quota 267 | private maintainConnections( 268 | quota: number, 269 | isDesiredPeer: (p: Peer) => boolean, 270 | pool: Parameters[0], 271 | isPeerRotatable: null | ((p: Peer) => boolean), 272 | rotationPeriod: number, 273 | ) { 274 | const query = this.ssb.conn.query(); 275 | const peersUp = query.peersConnected().filter(isDesiredPeer); 276 | const peersDown = query 277 | .peersConnectable(pool) 278 | .filter(isDesiredPeer) 279 | .filter(this.isNotBlocked) 280 | .filter(isNotLocalhost) 281 | .filter(([, data]) => data.autoconnect !== false); 282 | const excess = peersUp.length > quota * 2 ? peersUp.length - quota : 0; 283 | const freeSlots = Math.max(quota - peersUp.length, 0); 284 | const fullPercent = 1 - freeSlots / quota; 285 | 286 | // Disconnect from excess, after some long and random delay 287 | z(peersUp) 288 | .z(filterOldExcess(excess)) 289 | .z(sortByOldestConnection) 290 | .z(take(excess)) 291 | .forEach(([addr]) => this.ssb.conn.disconnect(addr)); 292 | 293 | // Disconnect from 1 peer that needs to rotate out to give others a chance. 294 | // The more there are other peers available, the shorter the grace period. 295 | const ROTATION_PERIOD = 296 | (rotationPeriod * RANDOM_MULTIPLIER) / Math.sqrt(peersDown.length); 297 | if ( 298 | freeSlots === 0 && 299 | peersDown.length > 0 && 300 | this.lastRotationAt + ROTATION_PERIOD < Date.now() 301 | ) { 302 | z(peersUp) 303 | .z(filter(isPeerRotatable ?? (() => true))) 304 | .z( 305 | filter(([, data]) => { 306 | const lastAttempt = data.stateChange ?? data.hubUpdated ?? 0; 307 | return lastAttempt + ROTATION_PERIOD < Date.now(); 308 | }), 309 | ) 310 | .z(sortByOldestConnection) 311 | .z(take(1)) 312 | .forEach(([addr]) => { 313 | this.lastRotationAt = Date.now(); 314 | this.ssb.conn.disconnect(addr); 315 | }); 316 | } 317 | 318 | // Connect to suitable candidates 319 | z(peersDown) 320 | .z(calculateCooldown(fullPercent, this.hops)) 321 | .z(filter(cooledDownEnough)) 322 | .z(sortByCooldownAscending) 323 | .z(take(freeSlots)) 324 | .forEach(([addr, data]) => this.ssb.conn.connect(addr, data)); 325 | } 326 | 327 | private updateStagingNow() { 328 | // Stage all db peers with autoconnect=false 329 | this.ssb.conn 330 | .query() 331 | .peersConnectable('db') 332 | .filter(this.isNotBlocked) 333 | .filter(([, data]) => data.autoconnect === false) 334 | .forEach(([addr, data]) => this.ssb.conn.stage(addr, data)); 335 | 336 | // Purge staged peers that are now blocked 337 | this.ssb.conn 338 | .query() 339 | .peersConnectable('staging') 340 | .filter(this.isBlocked) 341 | .forEach(([addr]) => this.ssb.conn.unstage(addr)); 342 | 343 | // Purge some old staged LAN peers 344 | this.ssb.conn 345 | .query() 346 | .peersConnectable('staging') 347 | .filter(([, data]) => data.type === 'lan') 348 | .filter(([, data]) => data.stagingUpdated! + 10e3 < Date.now()) 349 | .forEach(([addr]) => this.ssb.conn.unstage(addr)); 350 | 351 | // Purge some old staged Bluetooth peers 352 | this.ssb.conn 353 | .query() 354 | .peersConnectable('staging') 355 | .filter(([, data]) => data.type === 'bt') 356 | .filter(([, data]) => data.stagingUpdated! + 30e3 < Date.now()) 357 | .forEach(([addr]) => this.ssb.conn.unstage(addr)); 358 | } 359 | 360 | private updateHubNow() { 361 | // Try to maintain connections with 5 rooms 362 | this.maintainConnections( 363 | 5, 364 | isRoom, 365 | 'db', 366 | // When a room is empty, start rotating to other rooms with 2min period 367 | (p) => p[1].onlineCount === 0, 368 | 2 * MINUTES, 369 | ); 370 | 371 | // Try to maintain connections with 4 non-rooms 372 | this.maintainConnections( 373 | 4, 374 | isNotRoom, 375 | 'dbAndStaging', 376 | // 2 hour nominal rotation period 377 | null, 378 | 2 * HOUR, 379 | ); 380 | 381 | // Purge connected peers that are now blocked 382 | this.ssb.conn 383 | .query() 384 | .peersInConnection() 385 | .filter(this.isBlocked) 386 | .forEach(([addr]) => this.ssb.conn.disconnect(addr)); 387 | 388 | // Purge some ongoing frustrating connection attempts 389 | this.ssb.conn 390 | .query() 391 | .peersInConnection() 392 | .filter((p) => this.ssb.conn.hub().getState(p[0]) === 'connecting') 393 | .filter((p) => p[1].stateChange! + this.maxWaitToConnect(p) < Date.now()) 394 | .forEach(([addr]) => this.ssb.conn.disconnect(addr)); 395 | } 396 | 397 | private updateNow() { 398 | if (this.closed) return; 399 | if (this.isCurrentlyDownloading()) return; 400 | if (!this.loadedHops) return; 401 | if (!hasNetwork()) return; 402 | 403 | this.updateStagingNow(); 404 | this.updateHubNow(); 405 | } 406 | 407 | private updateSoon(period: number = 1000) { 408 | if (this.closed) return; 409 | if (this.hasScheduledAnUpdate) return; 410 | 411 | // Add some time randomization to avoid deadlocks with remote peers 412 | const fuzzyPeriod = period * 0.5 + period * Math.random(); 413 | this.hasScheduledAnUpdate = true; 414 | const timer = setTimeout(() => { 415 | this.updateNow(); 416 | this.hasScheduledAnUpdate = false; 417 | }, fuzzyPeriod); 418 | if (timer.unref) timer.unref(); 419 | } 420 | 421 | private removeDefunct(addr: string) { 422 | this.ssb.conn.db().update(addr, {defunct: void 0, autoconnect: void 0}); 423 | } 424 | 425 | private setupBluetoothDiscovery() { 426 | if (!this.ssb.bluetooth?.nearbyScuttlebuttDevices) { 427 | debug('Warning: ssb-bluetooth is missing, scheduling is degraded'); 428 | return; 429 | } 430 | 431 | interface BTPeer { 432 | remoteAddress: string; 433 | id: string; 434 | displayName: string; 435 | } 436 | 437 | pull( 438 | this.ssb.bluetooth.nearbyScuttlebuttDevices(1000), 439 | pull.drain(({discovered}: {discovered: Array}) => { 440 | if (this.closed) return; 441 | 442 | for (const btPeer of discovered) { 443 | const addr = 444 | `bt:${btPeer.remoteAddress.split(':').join('')}` + 445 | '~' + 446 | `shs:${btPeer.id.replace(/^\@/, '').replace(/\.ed25519$/, '')}`; 447 | const data: Partial = { 448 | type: 'bt', 449 | note: btPeer.displayName, 450 | key: btPeer.id, 451 | }; 452 | if (this.isNotBlocked([addr, data]) && this.isNotConnected(addr)) { 453 | this.ssb.conn.stage(addr, data); 454 | this.updateSoon(100); 455 | } 456 | } 457 | }), 458 | ); 459 | } 460 | 461 | private setupLanDiscovery() { 462 | if (!this.ssb.lan?.start || !this.ssb.lan?.discoveredPeers) { 463 | debug('Warning: ssb-lan is missing, scheduling is degraded'); 464 | return; 465 | } 466 | 467 | pull( 468 | this.ssb.lan.discoveredPeers(), 469 | pull.drain(({address, verified}: LANDiscovery) => { 470 | const key: FeedId | undefined = Ref.getKeyFromAddress(address); 471 | if (!key) return; 472 | const data: Partial = { 473 | type: 'lan', 474 | key, 475 | verified, 476 | }; 477 | if ( 478 | this.isNotBlocked([address, data]) && 479 | this.isNotConnected(address) 480 | ) { 481 | this.ssb.conn.stage(address, data); 482 | this.updateSoon(100); 483 | } 484 | }), 485 | ); 486 | 487 | this.ssb.lan.start(); 488 | } 489 | 490 | private setupRoomAttendantDiscovery() { 491 | const timer = setTimeout(() => { 492 | if (!this.ssb.roomClient?.discoveredAttendants) { 493 | debug('Warning: ssb-room-client@2 is missing, scheduling is degraded'); 494 | return; 495 | } 496 | 497 | interface Attendant { 498 | key: FeedId; 499 | address: string; 500 | room: FeedId; 501 | roomName?: string; 502 | } 503 | 504 | pull( 505 | this.ssb.roomClient.discoveredAttendants(), 506 | pull.drain((attendant: Attendant) => { 507 | const addr = attendant.address; 508 | const data: Partial = { 509 | type: 'room-attendant', 510 | key: attendant.key, 511 | room: attendant.room, 512 | roomName: attendant.roomName, 513 | }; 514 | if (this.isNotBlocked([addr, data]) && this.isNotConnected(addr)) { 515 | this.ssb.conn.stage(addr, data); 516 | this.updateSoon(100); 517 | } 518 | }), 519 | ); 520 | }, 100); 521 | timer?.unref?.(); 522 | } 523 | 524 | private setupPubDiscovery() { 525 | if (this.config.conn?.populatePubs === false) return; 526 | 527 | if (!this.ssb.db?.operators) { 528 | debug('Warning: ssb-db2 is missing, scheduling is degraded'); 529 | return; 530 | } 531 | 532 | const timer = setTimeout(() => { 533 | if (this.closed) return; 534 | if (!this.ssb.db?.operators) return; 535 | type PubContent = {address?: string}; 536 | const MAX_STAGED_PUBS = 3; 537 | const {where, type, live, toPullStream} = this.ssb.db.operators; 538 | this.pubDiscoveryPausable = this.pubDiscoveryPausable ?? Pausable(); 539 | 540 | pull( 541 | this.ssb.db!.query( 542 | where(type('pub')), 543 | live({old: true}), 544 | toPullStream(), 545 | ), 546 | pull.filter((msg: Msg) => 547 | Ref.isAddress(msg.value.content?.address), 548 | ), 549 | // Don't drain that fast, so to give other DB draining tasks priority 550 | pull.asyncMap((x: any, cb: any) => setTimeout(() => cb(null, x), 250)), 551 | this.pubDiscoveryPausable, 552 | pull.drain((msg: Msg) => { 553 | try { 554 | const address = Ref.toMultiServerAddress(msg.value.content.address); 555 | const key = Ref.getKeyFromAddress(address); 556 | if (this.isBlocked([address, {key}])) { 557 | this.ssb.conn.forget(address); 558 | } else if (!this.ssb.conn.db().has(address)) { 559 | this.ssb.conn.stage(address, {key, type: 'pub'}); 560 | this.ssb.conn.remember(address, { 561 | key, 562 | type: 'pub', 563 | autoconnect: false, 564 | }); 565 | } 566 | } catch (err) { 567 | debug('cannot process discovered pub because: %s', err); 568 | } 569 | }), 570 | ); 571 | 572 | // Pause or resume the draining depending on the number of staged pubs 573 | pull( 574 | this.ssb.conn.staging().liveEntries(), 575 | pull.drain((staged: Array) => { 576 | if (this.closed) return; 577 | 578 | const stagedPubs = staged.filter(([, data]) => data.type === 'pub'); 579 | if (stagedPubs.length >= MAX_STAGED_PUBS) { 580 | this.pubDiscoveryPausable?.pause(); 581 | } else { 582 | this.pubDiscoveryPausable?.resume(); 583 | } 584 | }), 585 | ); 586 | }, 1000); 587 | timer?.unref?.(); 588 | } 589 | 590 | private cleanUpDB() { 591 | const roomsWithMembership = new Set(); 592 | 593 | for (let peer of this.ssb.conn.dbPeers()) { 594 | const [address, {source, type, membership}] = peer; 595 | if ( 596 | source === 'local' || 597 | source === 'bt' || 598 | type === 'lan' || 599 | type === 'bt' 600 | ) { 601 | this.ssb.conn.forget(address); 602 | } 603 | if (isDefunct(peer)) { 604 | this.removeDefunct(address); 605 | } 606 | if (type === 'room' && membership) { 607 | roomsWithMembership.add(address); 608 | } 609 | } 610 | 611 | // Remove "room attendant aliases" that are in rooms where I'm a member 612 | for (let [address, data] of this.ssb.conn.dbPeers()) { 613 | if (data.type === 'room-endpoint' || data.type === 'room-attendant') { 614 | if ( 615 | data.alias && 616 | data.roomAddress && 617 | roomsWithMembership.has(data.roomAddress) 618 | ) { 619 | this.ssb.conn.forget(address); 620 | } 621 | } 622 | } 623 | } 624 | 625 | @muxrpc('sync') 626 | public start = () => { 627 | if (!this.closed) return; 628 | this.closed = false; 629 | 630 | // Upon init, purge some undesired DB entries 631 | this.cleanUpDB(); 632 | 633 | this.ssbDB2Subscription = this.ssb.db?.onMsgAdded?.(({kvt}: {kvt: Msg}) => { 634 | if (kvt.value.author !== this.ssb.id) { 635 | this.lastMessageAt = Date.now(); 636 | } 637 | }); 638 | 639 | // Upon init, load some follow and block data 640 | this.loadSocialGraph(); 641 | 642 | // Upon init, setup discovery via various modes 643 | this.setupBluetoothDiscovery(); 644 | this.setupLanDiscovery(); 645 | this.setupRoomAttendantDiscovery(); 646 | this.setupPubDiscovery(); 647 | 648 | // Upon regular time intervals, attempt to make connections 649 | this.intervalForUpdate = setInterval(() => this.updateSoon(), 2e3); 650 | this.intervalForUpdate?.unref?.(); 651 | 652 | // Upon wakeup, trigger hard reconnect 653 | onWakeup(() => { 654 | if (!this.closed) this.ssb.conn.hub().reset(); 655 | }); 656 | 657 | // Upon network changes, trigger hard reconnect 658 | onNetwork(() => { 659 | if (!this.closed) this.ssb.conn.hub().reset(); 660 | }); 661 | 662 | // Upon some disconnection, attempt to make connections 663 | pull( 664 | this.ssb.conn.hub().listen(), 665 | pull.filter((ev: HubEvent) => ev.type === 'disconnected'), 666 | pull.drain(() => this.updateSoon(200)), 667 | ); 668 | 669 | // Upon init, attempt to make some connections 670 | this.updateNow(); 671 | }; 672 | 673 | @muxrpc('sync') 674 | public stop = () => { 675 | this.pubDiscoveryPausable?.pause(); 676 | this.ssb.lan?.stop(); 677 | this.ssbDB2Subscription?.(); 678 | if (this.intervalForUpdate) { 679 | clearInterval(this.intervalForUpdate); 680 | this.intervalForUpdate = void 0; 681 | } 682 | this.ssb.conn.hub().reset(); 683 | this.closed = true; 684 | }; 685 | } 686 | --------------------------------------------------------------------------------