diff --git a/lib/index.js b/lib/index.js index feacd08..53d6c6a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,5 @@ +const makeDebug = require('debug') + /** * @typedef {ReturnType} PPPPPDB * @typedef {ReturnType} PPPPPGoal @@ -81,6 +83,8 @@ function initConductor(peer, config) { assertGCPlugin(peer) assertSyncPlugin(peer) + const debug = makeDebug('ppppp:conductor') + /** * Set replication goals for various tangles of an account: * - Account tangle @@ -111,6 +115,38 @@ function initConductor(peer, config) { const feedID = peer.db.feed.getID(accountID, domain) peer.goals.set(feedID, goalDSL) } + + // prettier-ignore + debug('Setup goals for %s@all, %s@set, %s@set, %s', accountID, followDomain, blockDomain, rules.join(', ')) + } + + /** + * @param {string} accountID + * @param {Array} rules + */ + function teardownAccountGoals(accountID, rules) { + assertDBPlugin(peer) + assertSetPlugin(peer) + assertGoalsPlugin(peer) + + peer.goals.set(accountID, 'none') + + const followDomain = peer.set.getDomain('follow') + const followFeedID = peer.db.feed.getID(accountID, followDomain) + peer.goals.set(followFeedID, 'none') + + const blockDomain = peer.set.getDomain('block') + const blockFeedID = peer.db.feed.getID(accountID, blockDomain) + peer.goals.set(blockFeedID, 'none') + + for (const rule of rules) { + const [domain] = parseRule(rule) + const feedID = peer.db.feed.getID(accountID, domain) + peer.goals.set(feedID, 'none') + } + + // prettier-ignore + debug('Teardown goals for %s@all, %s@set, %s@set, %s', accountID, followDomain, blockDomain, rules.join(', ')) } /** @@ -135,14 +171,25 @@ function initConductor(peer, config) { setupAccountGoals(myID, myRules) - // TODO: watch the set for live updates, on add, setupAccountGoals() - // TODO: watch the set for live updates, on remove, teardownAccountGoals() const followedAccounts = peer.set.values('follow') for (const theirID of followedAccounts) { setupAccountGoals(theirID, theirRules) } + // @ts-ignore + peer.set.watch(({ event, subdomain, value }) => { + const theirID = value + if (subdomain === 'follow' && event === 'add') { + setupAccountGoals(theirID, theirRules) + } + if (subdomain === 'follow' && event === 'del') { + teardownAccountGoals(theirID, theirRules) + } + if (subdomain === 'block' && event === 'add') { + teardownAccountGoals(theirID, theirRules) + } + }) - peer.gc.stop() + peer.gc.stop() // TODO: This should happen automatically in gc.start() peer.gc.start(maxBytes) peer.sync.start() } diff --git a/test/follow-feeds.test.js b/test/follow-feeds.test.js index 83567e4..2257b5c 100644 --- a/test/follow-feeds.test.js +++ b/test/follow-feeds.test.js @@ -176,3 +176,177 @@ test('GC selected feeds of followed accounts', async (t) => { await p(bob.close)(true) await p(carol.close)(true) }) + +test('GC recently-unfollowed accounts', async (t) => { + // Alice + const alice = createPeer({ name: 'alice' }) + await alice.db.loaded() + // Alice creates her own account + const aliceID = await p(alice.db.account.create)({ + subdomain: 'account', + _nonce: 'alice', + }) + await p(alice.set.load)(aliceID) + // Alice creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(alice.db.feed.publish)({ + account: aliceID, + domain: 'post', + data: { text: 'A' + i }, + }) + } + + // Bob + const bob = createPeer({ name: 'bob' }) + await bob.db.loaded() + // Bob creates his own account + const bobID = await p(bob.db.account.create)({ + subdomain: 'account', + _nonce: 'bob', + }) + await p(bob.set.load)(bobID) + // Bob creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(bob.db.feed.publish)({ + account: bobID, + domain: 'post', + data: { text: 'B' + i }, + }) + } + + // Carol + const carol = createPeer({ name: 'carol' }) + await carol.db.loaded() + // Carol creates her own account + const carolID = await p(carol.db.account.create)({ + subdomain: 'account', + _nonce: 'carol', + }) + await p(carol.set.load)(bobID) + // Carol creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(carol.db.feed.publish)({ + account: carolID, + domain: 'post', + data: { text: 'C' + i }, + }) + } + + // Alice follows Bob, but not Carol + assert(await p(alice.set.add)('follow', bobID), 'alice follows bob') + + alice.conductor.start(aliceID, [['post@all'], ['post@all']], 4_000) + bob.conductor.start(bobID, [['post@all'], ['post@all']], 4_000) + + const aliceDialingBob = await p(alice.connect)(bob.getAddress()) + const aliceDialingCarol = await p(alice.connect)(carol.getAddress()) + await p(setTimeout)(2000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B0', 'B1', 'B2', 'B3', 'B4'], + 'alice has alice and bob posts' + ) + + assert(await p(alice.set.del)('follow', bobID), 'alice unfollows bob') + await p(setTimeout)(1000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4'], + 'alice has alice posts' + ) + + await p(aliceDialingBob.close)(true) + await p(aliceDialingCarol.close)(true) + await p(alice.close)(true) + await p(bob.close)(true) + await p(carol.close)(true) +}) + +test('GC recently-blocked accounts', async (t) => { + // Alice + const alice = createPeer({ name: 'alice' }) + await alice.db.loaded() + // Alice creates her own account + const aliceID = await p(alice.db.account.create)({ + subdomain: 'account', + _nonce: 'alice', + }) + await p(alice.set.load)(aliceID) + // Alice creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(alice.db.feed.publish)({ + account: aliceID, + domain: 'post', + data: { text: 'A' + i }, + }) + } + + // Bob + const bob = createPeer({ name: 'bob' }) + await bob.db.loaded() + // Bob creates his own account + const bobID = await p(bob.db.account.create)({ + subdomain: 'account', + _nonce: 'bob', + }) + await p(bob.set.load)(bobID) + // Bob creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(bob.db.feed.publish)({ + account: bobID, + domain: 'post', + data: { text: 'B' + i }, + }) + } + + // Carol + const carol = createPeer({ name: 'carol' }) + await carol.db.loaded() + // Carol creates her own account + const carolID = await p(carol.db.account.create)({ + subdomain: 'account', + _nonce: 'carol', + }) + await p(carol.set.load)(bobID) + // Carol creates a feed of posts + for (let i = 0; i < 5; i++) { + await p(carol.db.feed.publish)({ + account: carolID, + domain: 'post', + data: { text: 'C' + i }, + }) + } + + // Alice follows Bob, but not Carol + assert(await p(alice.set.add)('follow', bobID), 'alice follows bob') + + alice.conductor.start(aliceID, [['post@all'], ['post@all']], 4_000) + bob.conductor.start(bobID, [['post@all'], ['post@all']], 4_000) + + const aliceDialingBob = await p(alice.connect)(bob.getAddress()) + const aliceDialingCarol = await p(alice.connect)(carol.getAddress()) + await p(setTimeout)(2000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B0', 'B1', 'B2', 'B3', 'B4'], + 'alice has alice and bob posts' + ) + + assert(await p(alice.set.add)('block', bobID), 'alice blocks bob') + await p(setTimeout)(1000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4'], + 'alice has alice posts' + ) + + await p(aliceDialingBob.close)(true) + await p(aliceDialingCarol.close)(true) + await p(alice.close)(true) + await p(bob.close)(true) + await p(carol.close)(true) +})