diff --git a/lib/index.js b/lib/index.js index 468ee2f..c9aaaf5 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,12 @@ /** * @typedef {ReturnType} PPPPPDB * @typedef {ReturnType} PPPPPGoal + * @typedef {import('ppppp-goals').GoalDSL} GoalDSL * @typedef {ReturnType} PPPPPSet * @typedef {ReturnType} PPPPPSync * @typedef {ReturnType} PPPPPGC - * @typedef {[Array, Array]} Rules + * @typedef {`${string}@${GoalDSL}`} Rule + * @typedef {[Array, Array]} Rules * @typedef {{ * db:PPPPPDB | null, * goals: PPPPPGoal | null, @@ -54,6 +56,20 @@ function assertSyncPlugin(peer) { if (!peer.sync) throw new Error('conductor plugin needs ppppp-sync plugin') } +/** + * @param {any} rule + * @returns {[string, GoalDSL]} + */ +function parseRule(rule) { + if (typeof rule !== 'string') throw new Error('rule must be a string') + if (!rule) throw new Error('rule must not be empty') + if (!rule.includes('@')) throw new Error('rule fit the format "domain@goal"') + const splitted = /**@type {[string, GoalDSL]}*/ (rule.split('@')) + if (!splitted[0]) throw new Error('rule must fit the format "domain@goal"') + if (!splitted[1]) throw new Error('rule must fit the format "domain@goal"') + return splitted +} + /** * @param {UnknownPeer} peer * @param {unknown} config @@ -72,10 +88,10 @@ function initConductor(peer, config) { * - Each tangle in the rule * * The "rule" is just a list of domains of feeds. - * @param {string} accountID - * @param {Array} rule + * @param {string} accountID ID of the account to set goals for + * @param {Array} rules list of feed domains of interest */ - function setupAccountGoals(accountID, rule) { + function setupAccountGoals(accountID, rules) { assertDBPlugin(peer) assertSetPlugin(peer) assertGoalsPlugin(peer) @@ -86,9 +102,14 @@ function initConductor(peer, config) { const followFeedID = peer.db.feed.getID(accountID, followDomain) peer.goals.set(followFeedID, 'set') - for (const domain of rule) { + const blockDomain = peer.set.getDomain('block') + const blockFeedID = peer.db.feed.getID(accountID, blockDomain) + peer.goals.set(blockFeedID, 'set') + + for (const rule of rules) { + const [domain, goalDSL] = parseRule(rule) const feedID = peer.db.feed.getID(accountID, domain) - peer.goals.set(feedID, 'all') // TODO better goal? + peer.goals.set(feedID, goalDSL) } } @@ -97,7 +118,7 @@ function initConductor(peer, config) { * Assumes that PPPPP Set has been loaded with the same accountID. * * @param {string} myID - * @param {Rules} rules + * @param {[Array, Array]} rules * @param {number} maxBytes */ function start(myID, rules, maxBytes) { @@ -107,20 +128,21 @@ function initConductor(peer, config) { assertGCPlugin(peer) assertSyncPlugin(peer) - const [myRule, theirRule] = rules + const [myRules, theirRules] = rules // TODO: Figure out goals for each tangle, and sizes according to maxLogBytes // TODO: Figure out ghost spans for dicts and sets - setupAccountGoals(myID, myRule) + setupAccountGoals(myID, myRules) // TODO: watch the set for live updates, on add, syncAccount() // TODO: watch the set for live updates, on remove, forgetAccount() const followedAccounts = peer.set.values('follow') for (const theirID of followedAccounts) { - setupAccountGoals(theirID, theirRule) + setupAccountGoals(theirID, theirRules) } + peer.gc.stop() peer.gc.start(maxBytes) peer.sync.start() } diff --git a/package.json b/package.json index 9c003e4..9abc548 100644 --- a/package.json +++ b/package.json @@ -33,11 +33,11 @@ "c8": "7", "ppppp-caps": "github:staltz/ppppp-caps", "ppppp-db": "github:staltz/ppppp-db", - "ppppp-dict": "github:staltz/ppppp-dict", + "ppppp-set": "github:staltz/ppppp-set", "ppppp-gc": "github:staltz/ppppp-gc", "ppppp-goals": "github:staltz/ppppp-goals", "ppppp-keypair": "github:staltz/ppppp-keypair", - "ppppp-tangle-sync": "github:staltz/ppppp-tangle-sync", + "ppppp-sync": "github:staltz/ppppp-sync", "prettier": "^2.6.2", "pretty-quick": "^3.1.3", "rimraf": "^4.4.0", diff --git a/test/follow-feeds.test.js b/test/follow-feeds.test.js index 4a6daac..83567e4 100644 --- a/test/follow-feeds.test.js +++ b/test/follow-feeds.test.js @@ -65,8 +65,8 @@ test('Replicate selected feeds of followed accounts', async (t) => { // Alice follows Bob, but not Carol assert(await p(alice.set.add)('follow', bobID), 'alice follows bob') - alice.conductor.start(aliceID, [['post'], ['post']], 64_000_000) - bob.conductor.start(bobID, [['post'], ['post']], 64_000_000) + alice.conductor.start(aliceID, [['post@all'], ['post@all']], 64_000_000) + bob.conductor.start(bobID, [['post@all'], ['post@all']], 64_000_000) const aliceDialingBob = await p(alice.connect)(bob.getAddress()) const aliceDialingCarol = await p(alice.connect)(carol.getAddress()) @@ -74,7 +74,7 @@ test('Replicate selected feeds of followed accounts', async (t) => { assert.deepEqual( getTexts([...alice.db.msgs()]), - ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B0', 'B1', 'B2', 'B3', 'B4'], + ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B0', 'B1', 'B2', 'B3', 'B4'], 'alice has alice and bob posts' ) @@ -84,3 +84,95 @@ test('Replicate selected feeds of followed accounts', async (t) => { await p(bob.close)(true) await p(carol.close)(true) }) + +test('GC selected feeds of followed 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']], 64_000_000) + bob.conductor.start(bobID, [['post@all'], ['post@all']], 64_000_000) + + const aliceDialingBob = await p(alice.connect)(bob.getAddress()) + const aliceDialingCarol = await p(alice.connect)(carol.getAddress()) + await p(setTimeout)(1000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B0', 'B1', 'B2', 'B3', 'B4'], + 'alice has alice and bob posts' + ) + + await p(aliceDialingBob.close)(true) + await p(aliceDialingCarol.close)(true) + + alice.conductor.start(aliceID, [['post@all'], ['post@newest-2']], 8_000) + const aliceDialingBob2 = await p(alice.connect)(bob.getAddress()) + const aliceDialingCarol2 = await p(alice.connect)(carol.getAddress()) + await p(setTimeout)(1000) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A0', 'A1', 'A2', 'A3', 'A4', /* */ 'B3', 'B4'], + 'alice has alice and bob posts' + ) + + await p(aliceDialingBob2.close)(true) + await p(aliceDialingCarol2.close)(true) + await p(alice.close)(true) + await p(bob.close)(true) + await p(carol.close)(true) +})