├── 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 | 
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 |
--------------------------------------------------------------------------------