From dad1d53207f5b5b151a049dafbaaab37698ec577 Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Sep 2022 14:08:21 +1300 Subject: [PATCH 1/8] add persistence tests --- test/api.test.js | 219 ++++++++++++++++++++++++++++------------------- 1 file changed, 129 insertions(+), 90 deletions(-) diff --git a/test/api.test.js b/test/api.test.js index 2be3d2a..508bd78 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -8,6 +8,29 @@ const { author, where, toCallback } = require('ssb-db2/operators') const { promisify: p } = require('util') const Testbot = require('./testbot.js') +/* Helpers */ + +function testReadAndPersisted(t, sbot, testRead) { + const { path } = sbot.config + + testRead(t, sbot, (err) => { + t.error(err, 'no error') + + console.log('> persistance') + + sbot.close(() => { + sbot = Testbot({ path, rimraf: false }) + testRead(t, sbot, (err) => { + t.error(err, 'no error') + sbot.close() + t.end() + }) + }) + }) +} + +/* Tests */ + test('getRoot() when there is nothing', (t) => { const sbot = Testbot() sbot.metafeeds.getRoot((err, found) => { @@ -17,11 +40,12 @@ test('getRoot() when there is nothing', (t) => { }) }) -test('findOrCreate(null, ...) can create the root metafeed', (t) => { +test('findOrCreate(null, null, null, cb)', (t) => { const sbot = Testbot() sbot.db.query( where(author(sbot.id)), toCallback((err, msgs) => { + if (err) throw err t.equals(msgs.length, 0, 'empty db') sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { @@ -36,6 +60,19 @@ test('findOrCreate(null, ...) can create the root metafeed', (t) => { ) }) +test('findOrCreate(cb)', (t) => { + const sbot = Testbot() + + sbot.metafeeds.findOrCreate((err, mf) => { + t.error(err, 'no err for findOrCreate()') + // t.equals(mf.feeds.length, 1, '1 sub feed in the root metafeed') + // t.equals(mf.feeds[0].feedpurpose, 'main', 'it is the main feed') + t.equals(mf.seed.toString('hex').length, 64, 'seed length is okay') + t.equals(typeof mf.keys.id, 'string', 'key seems okay') + sbot.close(true, t.end) + }) +}) + test('findOrCreate is idempotent', (t) => { const sbot = Testbot() sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { @@ -87,6 +124,7 @@ test('findOrCreate() a sub feed', (t) => { test('all FeedDetails have same format', (t) => { const sbot = Testbot() sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + if (err) throw err sbot.metafeeds.getRoot((err, mf) => { if (err) throw err sbot.metafeeds.findOrCreate( @@ -149,7 +187,6 @@ test('findOrCreate() a subfeed under a sub meta feed', (t) => { metadata: { query: 'foo' }, }, (err, f) => { - testIndexFeed = f.subfeed t.error(err, 'no err') t.equals(f.feedpurpose, 'index', 'it is the index subfeed') t.equals(f.metadata.query, 'foo', 'query is okay') @@ -202,38 +239,42 @@ async function setupTree(sbot) { test('findById', (t) => { const sbot = Testbot() + setupTree(sbot).then(({ indexF, indexesMF }) => { sbot.metafeeds.findById(null, (err, details) => { t.match(err.message, /feedId should be provided/, 'error about feedId') t.notOk(details) - sbot.metafeeds.findById(indexF.keys.id, (err, details) => { - t.error(err, 'no err') - t.deepEquals(Object.keys(details), [ - 'feedformat', - 'feedpurpose', - 'metafeed', - 'metadata', - ]) - t.equals(details.feedpurpose, 'index') - t.equals(details.metafeed, indexesMF.keys.id) - t.equals(details.feedformat, 'indexed-v1') - - sbot.close(true, t.end) + testReadAndPersisted(t, sbot, (t, sbot, cb) => { + sbot.metafeeds.findById(indexF.keys.id, (err, details) => { + if (err) return cb(err) + + t.deepEquals(Object.keys(details), [ + 'feedformat', + 'feedpurpose', + 'metafeed', + 'metadata', + ]) + t.equals(details.feedpurpose, 'index') + t.equals(details.metafeed, indexesMF.keys.id) + t.equals(details.feedformat, 'indexed-v1') + + cb(null) + }) }) }) }) }) -test('branchStream and restart', (t) => { - let sbot = Testbot() - const { path } = sbot.config +test('branchStream', (t) => { + const sbot = Testbot() - function testBranchStream(cb) { + function testRead(t, sbot, cb) { pull( sbot.metafeeds.branchStream({ old: true, live: false }), pull.collect((err, branches) => { - t.error(err, 'no err') + if (err) return cb(err) + t.equal(branches.length, 5, '5 branches') t.equal(branches[0].length, 1, 'root mf alone') @@ -252,26 +293,18 @@ test('branchStream and restart', (t) => { t.equal(branches[4].length, 3, 'index branch') t.equal(branches[4][2][1].feedpurpose, 'index', 'indexes branch') - cb() + cb(null) }) ) } setupTree(sbot).then(() => { - testBranchStream(() => { - sbot.close(true, () => { - t.pass('restart sbot') - sbot = Testbot({ path, rimraf: false }) - testBranchStream(() => { - sbot.close(true, t.end) - }) - }) - }) + testReadAndPersisted(t, sbot, testRead) }) }) test('findAndTombstone and tombstoning branchStream', (t) => { - let sbot = Testbot() + const sbot = Testbot() setupTree(sbot).then(({ rootMF }) => { pull( @@ -286,48 +319,52 @@ test('findAndTombstone and tombstoning branchStream', (t) => { t.equals(branch[1][1].feedpurpose, 'chess', 'live') t.equals(branch[1][1].reason, 'This game is too good', 'live') - pull( - sbot.metafeeds.branchStream({ - tombstoned: true, - old: true, - live: false, - }), - pull.drain((branch) => { - t.equals(branch.length, 2) - t.equals(branch[0][0], rootMF.keys.id, 'tombstoned: true') - t.equals(branch[1][1].feedpurpose, 'chess', 'tombstoned: true') - t.equals( - branch[1][1].reason, - 'This game is too good', - 'tombstoned: true' - ) + function testRead(t, sbot, cb) { + pull( + sbot.metafeeds.branchStream({ + tombstoned: true, + old: true, + live: false, + }), + pull.drain((branch) => { + t.equals(branch.length, 2) + t.equals(branch[0][0], rootMF.keys.id, 'tombstoned: true') + t.equals(branch[1][1].feedpurpose, 'chess', 'tombstoned: true') + t.equals( + branch[1][1].reason, + 'This game is too good', + 'tombstoned: true' + ) - pull( - sbot.metafeeds.branchStream({ - tombstoned: false, - old: true, - live: false, - }), - pull.collect((err, branches) => { - if (err) throw err - t.equal(branches.length, 4, 'tombstoned: false') - - pull( - sbot.metafeeds.branchStream({ - tombstoned: null, - old: true, - live: false, - }), - pull.collect((err, branches) => { - if (err) throw err - t.equal(branches.length, 5, 'tombstoned: null') - sbot.close(true, t.end) - }) - ) - }) - ) - }) - ) + pull( + sbot.metafeeds.branchStream({ + tombstoned: false, + old: true, + live: false, + }), + pull.collect((err, branches) => { + if (err) return cb(err) + t.equal(branches.length, 4, 'tombstoned: false') + + pull( + sbot.metafeeds.branchStream({ + tombstoned: null, + old: true, + live: false, + }), + pull.collect((err, branches) => { + if (err) return cb(err) + t.equal(branches.length, 5, 'tombstoned: null') + cb(null) + }) + ) + }) + ) + }) + ) + } + + testReadAndPersisted(t, sbot, testRead) }) ) @@ -349,26 +386,28 @@ test('findOrCreate() recps', (t) => { '30720d8f9cbf37f6d7062826f6decac93e308060a8aaaa77e6a4747f40ee1a76', 'hex' ) - sbot.box2.setOwnDMKey(testkey) - sbot.metafeeds.findOrCreate((err, mf) => { - sbot.metafeeds.findOrCreate( - mf, - (f) => f.feedpurpose === 'private', - { - feedpurpose: 'private', - feedformat: 'classic', - metadata: { - recps: [sbot.id], + testReadAndPersisted(t, sbot, (t, sbot, cb) => { + sbot.metafeeds.findOrCreate((err, mf) => { + if (err) return cb(err) + sbot.metafeeds.findOrCreate( + mf, + (f) => f.feedpurpose === 'private', + { + feedpurpose: 'private', + feedformat: 'classic', + metadata: { + recps: [sbot.id], + }, }, - }, - (err, f) => { - t.error(err, 'no err') - t.equal(f.feedpurpose, 'private') - t.equal(f.metadata.recps[0], sbot.id) - sbot.close(true, t.end) - } - ) + (err, f) => { + if (err) return cb(err) + t.equal(f.feedpurpose, 'private') + t.equal(f.metadata.recps[0], sbot.id) + cb(null) + } + ) + }) }) }) From 8a05f9922c931aabb9e4ea880f6c8d86423a3f27 Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Sep 2022 14:12:07 +1300 Subject: [PATCH 2/8] update the README --- README.md | 252 ++++++++++++--------------------------------- README_ADVANCED.md | 133 ++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 189 deletions(-) create mode 100644 README_ADVANCED.md diff --git a/README.md b/README.md index b078872..14b6721 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ main classDef default fill:#3f506c,stroke:#3f506c,color:#fff; ``` -How "classic" scuttlebutt worked - each device has one `main` feed with all messages +_How "classic" scuttlebutt worked - each device has one `main` feed with all messages_ ```mermaid graph TB @@ -37,19 +37,20 @@ classDef root fill:#8338ec,stroke:#8338ec,color:#fff; classDef default fill:#3a86ff,stroke:#3a86ff,color:#fff; classDef legacy fill:#3f506c,stroke:#3f506c,color:#fff; ``` -How scuttlebutt works with metafeeds - each device now has a `root` metafeed, +_How scuttlebutt works with metafeeds - each device now has a `root` metafeed, whose sole responsibility is to announce (point to) subfeeds that you publish content to. A subfeed can also be a metafeed, which then allows the existence of -"sub-subfeeds". +"sub-subfeeds"._ -This means that when you first meet a peer you can replicate their `root` metafeed -and, having discovered their subfeeds, replicate just their `aboutMe` and `contacts` feeds -to get enough info to place them socially. Once you decide you want to follow them you may -replicate their other subfeeds. +This means that when you first meet a peer you can replicate their `root` +metafeed and, having discovered their subfeeds, replicate just their `aboutMe` +and `contacts` feeds to get enough info to place them socially. Once you decide +you want to follow them you may replicate their other subfeeds. -_NOTE: The ideal state is that all content is split out into subfeeds. -To add backwards compatability for devices that have already posted a lot of posts to their -classic `main` feed, this library will auto-link that main feed in as a "subfeed" of our root._ +_NOTE: The ideal state is that all content is split out into subfeeds. To add +backwards compatability for devices that have already posted a lot of posts to +their classic `main` feed, this library will auto-link that main feed in as a +"subfeed" of our root._ ## Installation @@ -74,117 +75,59 @@ Add this plugin like this: ## Example usage -Let's start by creating a **root metafeed** with `findOrCreate(cb)`, necessary -for using this module. There can only be one _root_ metafeed, so even if you -call `findOrCreate(cb)` many times, it will not create duplicates, it will just -load the root metafeed. +We create a subfeed for `about` messages under our `root` feed using +`findOrCreate`. This will only create the subfeed if there is no existing +subfeed that matches the criteria. ```js -sbot.metafeeds.findOrCreate((err, root) => { - // ... -}) -``` +const details = { feedpurpose: 'aboutMe' } +sbot.metafeeds.findOrCreate(details, (err, aboutMeFeed) => { + console.log(aboutMeFeed) -Now we create a subfeed for `about` messages under our `root` feed using -`findOrCreate(parent, isFeed, details, cb)`. This will only create the subfeed -if there is no existing subfeed that matches the criteria`isFeed`: - -```js -sbot.metafeeds.findOrCreate((err, root) => { - // Find an existing feed - const isFeed = (feed) => feed.feedpurpose === 'aboutMe' - // Details for creating a subfeed if it doesn't already exist: - const details = { feedpurpose: 'aboutMe', feedformat: 'classic' } - - sbot.metafeeds.findOrCreate(root, isFeed, details, (err, aboutMeFeed) => { - // ... - }) + // }) ``` -The `details` argument is a *FeedDetails* object and it always requires -`feedpurpose` (any string) and `feedformat` (either `'classic'` for normal -ed25519 SSB feeds, or `'bendybutt-v1'` if you want a metafeed). The `isFeed` -function also takes a *FeedDetails* object as input. +The `details` argument is an object used to find (or create) a subfeed under +your "root feed". (It actually nests it under a couple of subfeeds, to handle +versioning, and sparse replication, but you generally don't need to know the +details). Once you have a *FeedDetails* object, like `aboutMeFeed`, you can publish on the new subfeed: ```js -sbot.metafeeds.findOrCreate((err, root) => { - const isFeed = (feed) => feed.feedpurpose === 'aboutMe' - const details = { feedpurpose: 'aboutMe', feedformat: 'classic' } - - sbot.metafeeds.findOrCreate(root, isFeed, details, (err, aboutMeFeed) => { - const content = { - type: 'about', - name: 'baba yaga' - description: 'lives in a hutt in the forest, swing by sometime!' - } - sbot.db.publishAs(aboutMeFeed.keys, content, (err, msg) => { - // ... - }) +const details = { feedpurpose: 'aboutMe' } +sbot.metafeeds.findOrCreate(details, (err, aboutMeFeed) => { + console.log(aboutMeFeed) + + const content = { + type: 'about', + name: 'baba yaga' + description: 'lives in a hutt in the forest, swing by sometime!' + } + sbot.db.create({ keys: aboutMeFeed.keys, content }, (err, msg) => { + console.log(msg) }) }) ``` ## API -### `sbot.metafeeds.findOrCreate(cb)` - -Calls back with your `root` Metafeed object which has the form: - -```js -{ - metafeed: null, - subfeed: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=', - feedpurpose: 'root', - feedformat: 'bendybutt-v1', - seed: , - keys: { - curve: 'ed25519', - public: 'sxK3OnHxdo7yGZ+28HrgpVq8nRBFaOCEGjRE4nB7CO8=.ed25519', - private: 'SOEx7hA9vRHrli0PZwNJ8jijH+PShmlrzz/JAKI7v6SzErc6cfF2jvIZn7bweuClWrydEEVo4IQaNETicHsI7w==.ed25519', - id: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=' - }, - metadata: {} -``` - -Meaning: -- `metafeed` - the id of the feed this is underneath. As this is the topmost feed, this is empty -- `subfeed` - the id of this feed, same as `keys.id` -- `feedpurpose` - a human readable ideally unique handle for this feed -- `feedformat` - the feed format ("classic" or "bendybutt-v1" are current options) -- `seed` - the data from which is use to derive the `keys` and `id` of this feed. -- `keys` - cryptographic keys used for signing messages published by this feed (see [ssb-keys]) -- `metadata` - additional data - -NOTES: -- the `root` metafeed is unique - you have only one, and it has no metafeed (it's at the top!) -- if you have a legacy `main` feed, this will also set that up as a subfeed of your `root` feed. - -### `sbot.metafeeds.findOrCreate(metafeed, isFeed, details, cb)` - -Looks for the first subfeed of `metafeed` that satisfies the condition in `isFeed`, -or creates it matching the properties in `details`. +### `sbot.metafeeds.findOrCreate(details, cb)` -This is strictly concerned with meta feeds and sub feeds that **you own**, not -with those that belong to other peers. +Looks for the first subfeed of `metafeed` that matches `details`, or creates +one which matches these. This creates feeds following the +[v1 tree structure](https://github.com/ssbc/ssb-meta-feeds-spec#v1). Arguments: -- `metafeed` - the metafeed you are finding/ creating under, can be: - - *FeedDetails* object (as returned by `findOrCreate()` or `getRoot()`) - - *null* which is short-hand for the `rootFeed` (this will be created if doesn't exist) -- `isFeed` - method you use to find an existing *FeedDetails*, can be: - - *function* of shape `(FeedDetails) => boolean` - - *null* - this method will then return an arbitrary subfeed under provided `metafeed` -- `details` - used to create a new subfeed if a match for an existing one is not found, can be - - *Object*: - - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed - - `details.feedformat` *String* either `'classic'` or `'bendybutt-v1'` - - `details.metadata` *Object* (optional) - for containing other data - - if `details.metadata.recps` is used, the subfeed announcement will be encrypted - - *null* - only allowed if `metafeed` is null (i.e. the details of the `root` FeedDetails) +- `details` *Object* where + - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed + - `details.feedformat` *String* (optional) + - either `'classic'` or `'bendybutt-v1'` + - default: `'classic'` + - `details.metadata` *Object* (optional) - for containing other data + - if `details.metadata.recps` is used, the subfeed announcement will be encrypted - `cb` *function* delivers the response, has signature `(err, FeedDetails)`, where FeedDetails is ```js { @@ -206,34 +149,29 @@ Arguments: } ``` -### `sbot.metafeeds.findById(feedId, cb)` +Meaning: +- `metafeed` - the id of the feed this is underneath +- `subfeed` - the id of this feed, same as `keys.id` +- `feedpurpose` - a human readable ideally unique handle for this feed +- `feedformat` - the feed format ("classic" or "bendybutt-v1" are current options) +- `seed` - the data from which is use to derive the `keys` and `id` of this feed. +- `keys` - cryptographic keys used for signing messages published by this feed (see [ssb-keys]) +- `metadata` - object containing additional data -Given a `feedId` that is presumed to be a subfeed of some meta feed, this API -fetches the *Details* object describing that feed, which is of form: +NOTES: +- if you have a legacy `main` feed, this will also set that up as a subfeed of your `root` feed. -```js -{ - metafeed, - feedpurpose, - feedformat, - id, - // seed - // keys - metadata -} -``` -NOTE - may include `seed`, `keys` if this is one of your feeds. +### `sbot.metafeeds.findOrCreate(cb)` -### `sbot.metafeeds.findByIdSync(feedId)` +Fetches the **root metafeed** details. There can only be one _root_ metafeed, +so even if you call `findOrCreate(cb)` many times, it will not create duplicates, +it will just load the root metafeed. -Similar to `findById`, but returns synchronously. :warning: Note, in order to -use this API, you **must** call `sbot.metafeeds.loadState(cb)` first, and wait -for `cb` to be called. +Callsback with your `root` FeedDetails object (see `findOrCreate(details, cb)`) -You can also call `sbot.metafeeds.ensureLoaded(feedId, cb)` on an individual -basis to make sure that `findByIdSync` will operate at the correct time when the -`feedId`'s metadata has been processed in the local database. +NOTES: +- `metafeed = null` - the root metafeed is the topmost metafeed ### `sbot.metafeeds.branchStream(opts)` @@ -296,73 +234,9 @@ The `opts` argument can have the following properties: in the results; if `true`, only tombstoned branches are included; if `null`, all branches are included regardless of tombstoning. (Default: `null`) +### Advanced API -### `sbot.metafeeds.findAndTombstone(metafeed, isFeed, reason, cb)` - -_Looks for the first subfeed of `metafeed` that satisfies the condition in -`isFeed` and, if found, tombstones it with the string `reason`. - -This is strictly concerned with meta feeds and sub feeds that **you own**, not -with those that belong to other peers. - -Arguments: -- `metafeed` *FeedDetails* object (as returned by e.g. `findOrCreate()`, `getRoot()`). -- `isFeed` *function* of the shape `(FeedDetails) => Boolean`. -- `reason` *String* - describes why the found feed is being tombstoned. - -The callback is called with `true` on the 2nd argument if tombstoning suceeded, -or called with an error object on the 1st argument if it failed. - - -### `sbot.metafeeds.getRoot(cb)` - -Looks for the root meta feed declared by your main feed, and returns it (as -`{ seed, keys}`) via the callback `cb` if it exists. - -If it does not exist, this API will **not** create the root meta feed. - - -## Validation - -Exposed via the internal API. - -### `isValid(msg, hmacKey)` - -_Validate a single meta feed message._ - -Extracts the `contentSection` from the given `msg` object and calls -`validateSingle()` to perform validation checks. - -If provided, the `hmacKey` is also given as input to the `validateSingle()` -function call. `hmacKey` may be `null` or a valid HMAC key supplied as a -`Buffer` or `string`. - -The response is a boolean: `true` if validation is successful, `false` if -validation fails in any way. Note that this function does not return the -underlying cause of the validation failure. - -### `validateSingle(contentSection, hmacKey)` - -_Validate a single meta feed message `contentSection` according to the criteria -defined in the [specification](https://github.com/ssb-ngi-pointer/ssb-meta-feed-spec#usage-of-bendy-butt-feed-format)._ - -`contentSection` must be an array of `content` and `contentSignature`. If a -`string` is provided (representing an encrypted message, for instance) an error -will be returned; an encrypted `contentSection` cannot be validated. - -`hmacKey` may be `null` or a valid HMAC key supplied as a `Buffer` or `string`. - -The response will be `undefined` (for successful validation) or an `Error` -object with a `message` describing the error. - -### `validateMetafeedAnnounce(msg)` - -_Validates a `metafeed/announce` message expected to be published on "main" -feeds which are in the classic format, but are signed by a meta feed according -to the [ssb meta feed spec]._ - -The response will be `undefined` (for successful validation) or an `Error` -object with a `message` describing the error. +For lower level API docs, [see here](./README_ADVANCED.md). ## License diff --git a/README_ADVANCED.md b/README_ADVANCED.md new file mode 100644 index 0000000..0655b90 --- /dev/null +++ b/README_ADVANCED.md @@ -0,0 +1,133 @@ +# Advanced API + +Most people using this module should not need to access these methods. +Some of them are low level and there for testing, some are for people wanting to step off +the recommended path. + +### `sbot.metafeeds.advanced.findOrCreate(metafeed, isFeed, details, cb)` + +Looks for the first subfeed of `metafeed` that satisfies the condition in `isFeed`, +or creates it matching the properties in `details`. + +This is strictly concerned with meta feeds and sub feeds that **you own**, not +with those that belong to other peers. + +Arguments: +- `metafeed` - the metafeed you are finding/ creating under, can be: + - *FeedDetails* object (as returned by `findOrCreate()` or `getRoot()`) + - *null* which is short-hand for the `rootFeed` (this will be created if doesn't exist) +- `isFeed` - method you use to find an existing *FeedDetails*, can be: + - *function* of shape `(FeedDetails) => boolean` + - *null* - this method will then return an arbitrary subfeed under provided `metafeed` +- `details` - used to create a new subfeed if a match for an existing one is not found, can be + - *Object*: + - `details.feedpurpose` *String* any string to characterize the purpose of this new subfeed + - `details.feedformat` *String* either `'classic'` or `'bendybutt-v1'` + - `details.metadata` *Object* (optional) - for containing other data + - if `details.metadata.recps` is used, the subfeed announcement will be encrypted + - *null* - only allowed if `metafeed` is null (i.e. the details of the `root` FeedDetails) +- `cb` *function* delivers the response, has signature `(err, FeedDetails)`, where FeedDetails is + ```js + { + metafeed: 'ssb:feed/bendybutt-v1/sxK3OnHxdo7yGZ-28HrgpVq8nRBFaOCEGjRE4nB7CO8=', + subfeed: '@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519', + feedpurpose: 'chess', + feedformat: 'classic', + seed: + keys: { + curve: 'ed25519', + public: 'I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519', + private: 'Mxa+LL16ws7HZhetR9FbsIOsAeud+ii+9KDUisXkq08jlMEfoG4K8yRIBYlcrBrYUR3zL99Rp+RDXPX0/JfNsQ==.ed25519', + id: '@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519' + }, + metadata: { // example + notes: 'private testing of chess dev', + recps: ['@I5TBH6BuCvMkSAWJXKwa2FEd8y/fUafkQ1z19PyXzbE=.ed25519'] + }, + } + ``` + +### `sbot.metafeeds.advanced.findById(feedId, cb)` + +Given a `feedId` that is presumed to be a subfeed of some meta feed, this API +fetches the *Details* object describing that feed, which is of form: + +```js +{ + metafeed, + feedpurpose, + feedformat, + id, + // seed + // keys + metadata +} +``` + +NOTE - may include `seed`, `keys` if this is one of your feeds. + +### `sbot.metafeeds.advanced.findAndTombstone(metafeed, isFeed, reason, cb)` + +_Looks for the first subfeed of `metafeed` that satisfies the condition in +`isFeed` and, if found, tombstones it with the string `reason`. + +This is strictly concerned with meta feeds and sub feeds that **you own**, not +with those that belong to other peers. + +Arguments: +- `metafeed` *FeedDetails* object (as returned by e.g. `findOrCreate()`, `getRoot()`). +- `isFeed` *function* of the shape `(FeedDetails) => Boolean`. +- `reason` *String* - describes why the found feed is being tombstoned. + +The callback is called with `true` on the 2nd argument if tombstoning suceeded, +or called with an error object on the 1st argument if it failed. + + +### `sbot.metafeeds.advanced.getRoot(cb)` + +Looks for the root meta feed declared by your main feed, and returns it (as +`{ seed, keys}`) via the callback `cb` if it exists. + +If it does not exist, this API will **not** create the root meta feed. + + +### `sbot.metafeeds.validate.isValid(msg, hmacKey)` + +_Validate a single meta feed message._ + +Extracts the `contentSection` from the given `msg` object and calls +`validateSingle()` to perform validation checks. + +If provided, the `hmacKey` is also given as input to the `validateSingle()` +function call. `hmacKey` may be `null` or a valid HMAC key supplied as a +`Buffer` or `string`. + +The response is a boolean: `true` if validation is successful, `false` if +validation fails in any way. Note that this function does not return the +underlying cause of the validation failure. + +### `sbot.metafeeds.validate.validateSingle(contentSection, hmacKey)` + +_Validate a single meta feed message `contentSection` according to the criteria +defined in the [specification](https://github.com/ssb-ngi-pointer/ssb-meta-feed-spec#usage-of-bendy-butt-feed-format)._ + +`contentSection` must be an array of `content` and `contentSignature`. If a +`string` is provided (representing an encrypted message, for instance) an error +will be returned; an encrypted `contentSection` cannot be validated. + +`hmacKey` may be `null` or a valid HMAC key supplied as a `Buffer` or `string`. + +The response will be `undefined` (for successful validation) or an `Error` +object with a `message` describing the error. + +### `sbot.metafeeds.validate.validateMetafeedAnnounce(msg)` + +_Validates a `metafeed/announce` message expected to be published on "main" +feeds which are in the classic format, but are signed by a meta feed according +to the [ssb meta feed spec]._ + +The response will be `undefined` (for successful validation) or an `Error` +object with a `message` describing the error. + + + From 90f81836546742c2093b57fef4f38edb5a186c38 Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Sep 2022 14:21:58 +1300 Subject: [PATCH 3/8] Squashed commit of the following: commit fdf2c98a402b8bdc959b4e662934177bdafe290d Merge: 1663678 fbe1e39 Author: mixmix Date: Tue Sep 27 13:52:33 2022 +1300 Merge branch 'auto-shard' of github.com:ssbc/ssb-meta-feeds into auto-shard commit 16636788db17df221a76928cf0ab9c722c6a01d2 Merge: fd31fb1 70527d5 Author: mixmix Date: Tue Sep 27 13:52:11 2022 +1300 Merge branch 'atomic-tests-cb-persist' into auto-shard commit 70527d56e55199b7f6d50d2ba87aff5685823593 Merge: 375bd22 8c9f464 Author: mixmix Date: Tue Sep 27 13:51:02 2022 +1300 Merge branch 'atomic-tests-cb-testbot' into atomic-tests-cb-persist commit 8c9f464120af19bb5ae1535f514f90671cdb4377 Merge: 8ef1d0e 0e7a624 Author: mixmix Date: Tue Sep 27 13:49:38 2022 +1300 Merge branch 'master' of github.com:ssbc/ssb-meta-feeds into atomic-tests-cb-testbot commit fbe1e39af9028b31a97ac3a95eef5f47fa4b86fd Author: Andre Staltz Date: Wed Sep 21 14:18:03 2022 +0300 remove a code comment from api.js commit fe51f9776677a6dd800a27cb6d887c58a43d04ea Author: Andre Staltz Date: Wed Sep 21 14:12:15 2022 +0300 remove a code comment from lookup.js commit fd31fb1b61338ad5c4fd5ab02bf68c9417659c03 Author: mixmix Date: Wed Sep 21 16:18:40 2022 +1200 tidy commit 4cad7a86e7880506d4ff48a09f0ab5592a7d97b1 Author: mixmix Date: Wed Sep 21 14:13:56 2022 +1200 cover recps case, update README commit 8ef1d0ec6582786476034c235f38a02ea6556f61 Author: mixmix Date: Wed Sep 21 12:25:43 2022 +1200 use testbot everywhere, group non-atomic tests commit de10af19bff643e0e2079471b410e5c81d0e46b6 Merge: 4e5accf 375bd22 Author: mixmix Date: Wed Sep 21 11:44:44 2022 +1200 Merge branch 'atomic-tests-cb-persist' into auto-shard commit 4e5accffe3896b9efcfc1cba14995b627ab4d722 Author: mixmix Date: Wed Sep 21 11:40:38 2022 +1200 fixup commit 375bd22b86f25692386f6d13089ca60293838718 Author: mixmix Date: Wed Sep 21 11:21:36 2022 +1200 add persistence tests to api commit 13a0f22035e21b3dc27d649b9f3126fd115bdae5 Author: Andre Staltz Date: Tue Sep 20 11:15:33 2022 +0300 revert promise tests to cb tests commit 8267fa8ecc70637ab7df6bd407b587285bc12b43 Author: mixmix Date: Tue Sep 20 17:33:45 2022 +1200 fixup commit ed724847abf439487012ffa4cc374844ef73c423 Author: mixmix Date: Tue Sep 20 17:28:01 2022 +1200 split out Advanced API to unclutter the README commit 4409661b1f1855bf231ace8ab0a37bdb59c118f7 Author: mixmix Date: Tue Sep 20 17:18:29 2022 +1200 fixups commit 6d4313cab8db60030292e7851d03de9870c5b060 Author: mixmix Date: Wed Sep 14 17:10:01 2022 +1200 test that shard-feed is created in correct place commit ba96d7f4c9050855bcce51dd244caac473fb18bb Author: mixmix Date: Wed Sep 14 16:44:11 2022 +1200 add API which auto-shards commit e4cb0c89aa4b567f8d00522603b7d37bb4e5b70f Author: mixmix Date: Tue Sep 20 16:54:41 2022 +1200 make tests atomic, add persistence tests --- api.js | 45 ++++++++++- index.js | 6 +- feeds-lookup.js => lookup.js | 0 messages.js | 2 +- package.json | 2 +- pick-shard.js | 15 ++++ test/api.test.js | 140 ++++++++++++++++++++++++++--------- test/pick-shard.test.js | 29 ++++++++ 8 files changed, 194 insertions(+), 45 deletions(-) rename feeds-lookup.js => lookup.js (100%) create mode 100644 pick-shard.js create mode 100644 test/pick-shard.test.js diff --git a/api.js b/api.js index c200a20..1b3d009 100644 --- a/api.js +++ b/api.js @@ -4,8 +4,17 @@ const run = require('promisify-tuple') const debug = require('debug')('ssb:meta-feeds') +const pickShard = require('./pick-shard') const alwaysTrue = () => true +const BB1 = 'bendybutt-v1' +const v1Details = { feedpurpose: 'v1', feedformat: BB1 } +const v1Visit = detailsToVisit(v1Details) +function detailsToVisit(details) { + return (feed) => + feed.feedpurpose === details.feedpurpose && + feed.feedformat === details.feedformat +} exports.init = function (sbot, config) { function filter(metafeed, visit, maybeCB) { @@ -210,11 +219,39 @@ exports.init = function (sbot, config) { } } + function commonFindOrCreate(details, cb) { + if (!details.feedformat) details.feedformat = 'classic' + + findOrCreate((err, rootFeed) => { + if (err) return cb(err) + + findOrCreate(rootFeed, v1Visit, v1Details, (err, v1Feed) => { + if (err) return cb(err) + + const shardDetails = { + feedpurpose: pickShard(rootFeed.keys.id, details.feedpurpose), + feedformat: BB1, + } + const shardVisit = detailsToVisit(shardDetails) + + findOrCreate(v1Feed, shardVisit, shardDetails, (err, shardFeed) => { + if (err) return cb(err) + + findOrCreate(shardFeed, detailsToVisit(details), details, cb) + }) + }) + }) + } + return { - getRoot, - findOrCreate, - findAndTombstone, - findById, branchStream, + findOrCreate: commonFindOrCreate, + + advanced: { + getRoot, + findOrCreate, + findAndTombstone, + findById, + }, } } diff --git a/index.js b/index.js index 68fcb06..2f28f7e 100644 --- a/index.js +++ b/index.js @@ -2,19 +2,19 @@ // // SPDX-License-Identifier: LGPL-3.0-only +const API = require('./api') const Keys = require('./keys') const Messages = require('./messages') +const Lookup = require('./lookup') const Query = require('./query') -const API = require('./api') const Validate = require('./validate') -const FeedsLookup = require('./feeds-lookup') exports.name = 'metafeeds' exports.init = function (sbot, config) { const messages = Messages.init(sbot, config) const query = Query.init(sbot, config) - const lookup = FeedsLookup.init(sbot, config) + const lookup = Lookup.init(sbot, config) const api = API.init(sbot, config) return { diff --git a/feeds-lookup.js b/lookup.js similarity index 100% rename from feeds-lookup.js rename to lookup.js diff --git a/messages.js b/messages.js index 02a0c42..67a1054 100644 --- a/messages.js +++ b/messages.js @@ -39,7 +39,7 @@ exports.init = function init(sbot) { return { feedFormat: 'bendybutt-v1', keys: mfKeys, - contentKeys: feedKeys, + contentKeys: feedKeys, // see ssb-bendy-butt/format.js content, encryptionFormat: 'box2', // in case metadata.recps is set } diff --git a/package.json b/package.json index 28d0ca6..1d71164 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,8 @@ "rimraf": "^3.0.2", "secret-stack": "^6.4.0", "ssb-bendy-butt": "^1.0.1", - "ssb-db2": "^6.1.1", "ssb-caps": "^1.1.0", + "ssb-db2": "^6.1.1", "tap-arc": "^0.3.5", "tape": "^5.6.0" }, diff --git a/pick-shard.js b/pick-shard.js new file mode 100644 index 0000000..3f24aed --- /dev/null +++ b/pick-shard.js @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2022 Mix Irving +// +// SPDX-License-Identifier: LGPL-3.0-only + +const bfe = require('ssb-bfe') +const crypto = require('crypto') + +module.exports = function pickShard(rootFeedId, idString) { + const buf = Buffer.concat([bfe.encode(rootFeedId), bfe.encode(idString)]) + + const hash = crypto.createHash('sha256') + hash.update(buf) + + return hash.digest('hex')[0] +} diff --git a/test/api.test.js b/test/api.test.js index 508bd78..e01edd7 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -31,16 +31,16 @@ function testReadAndPersisted(t, sbot, testRead) { /* Tests */ -test('getRoot() when there is nothing', (t) => { +test('advanced.getRoot() when there is nothing', (t) => { const sbot = Testbot() - sbot.metafeeds.getRoot((err, found) => { + sbot.metafeeds.advanced.getRoot((err, found) => { t.error(err, 'no err for find()') t.notOk(found, 'nothing found') sbot.close(true, t.end) }) }) -test('findOrCreate(null, null, null, cb)', (t) => { +test('advanced.findOrCreate(null, null, null, cb)', (t) => { const sbot = Testbot() sbot.db.query( where(author(sbot.id)), @@ -48,7 +48,7 @@ test('findOrCreate(null, null, null, cb)', (t) => { if (err) throw err t.equals(msgs.length, 0, 'empty db') - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { t.error(err, 'no err for findOrCreate()') // t.equals(mf.feeds.length, 1, '1 sub feed in the root metafeed') // t.equals(mf.feeds[0].feedpurpose, 'main', 'it is the main feed') @@ -60,10 +60,10 @@ test('findOrCreate(null, null, null, cb)', (t) => { ) }) -test('findOrCreate(cb)', (t) => { +test('advanced.findOrCreate(cb)', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { t.error(err, 'no err for findOrCreate()') // t.equals(mf.feeds.length, 1, '1 sub feed in the root metafeed') // t.equals(mf.feeds[0].feedpurpose, 'main', 'it is the main feed') @@ -73,19 +73,19 @@ test('findOrCreate(cb)', (t) => { }) }) -test('findOrCreate is idempotent', (t) => { +test('advanced.findOrCreate is idempotent', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { t.error(err, 'no err for findOrCreate()') t.ok(mf, 'got a metafeed') - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.getRoot((err, mf) => { t.error(err, 'no err for getRoot()') t.equals(mf.seed.toString('hex').length, 64, 'seed length is okay') t.equals(typeof mf.keys.id, 'string', 'key seems okay') const originalSeed = mf.seed.toString('hex') const originalID = mf.keys.id - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { t.error(err, 'no err for findOrCreate(null, ...)') t.equals(mf.seed.toString('hex'), originalSeed, 'same seed') t.equals(mf.keys.id, originalID, 'same ID') @@ -96,14 +96,14 @@ test('findOrCreate is idempotent', (t) => { }) }) -test('findOrCreate() a sub feed', (t) => { +test('advanced.findOrCreate() a sub feed', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.getRoot((err, mf) => { t.error(err, 'gets rootFeed') // lets create a new chess feed - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'chess', { @@ -123,11 +123,11 @@ test('findOrCreate() a sub feed', (t) => { test('all FeedDetails have same format', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, mf) => { + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, mf) => { if (err) throw err - sbot.metafeeds.getRoot((err, mf) => { + sbot.metafeeds.advanced.getRoot((err, mf) => { if (err) throw err - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( null, () => true, {}, @@ -140,7 +140,7 @@ test('all FeedDetails have same format', (t) => { 'getRoot and findOrCreate return the same root FeedDetails' ) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'chess', { @@ -163,10 +163,10 @@ test('all FeedDetails have same format', (t) => { }) }) -test('findOrCreate() a subfeed under a sub meta feed', (t) => { +test('advanced.findOrCreate() a subfeed under a sub meta feed', (t) => { const sbot = Testbot() - sbot.metafeeds.findOrCreate(null, null, null, (err, rootMF) => { - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate(null, null, null, (err, rootMF) => { + sbot.metafeeds.advanced.findOrCreate( rootMF, (f) => f.feedpurpose === 'indexes', { feedpurpose: 'indexes', feedformat: 'bendybutt-v1' }, @@ -178,7 +178,7 @@ test('findOrCreate() a subfeed under a sub meta feed', (t) => { 'has a bendy butt SSB URI' ) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( indexesMF, (f) => f.feedpurpose === 'index', { @@ -209,8 +209,8 @@ test('findOrCreate() a subfeed under a sub meta feed', (t) => { // - indexes // - about async function setupTree(sbot) { - const rootMF = await p(sbot.metafeeds.findOrCreate)() - const chessF = await p(sbot.metafeeds.findOrCreate)( + const rootMF = await p(sbot.metafeeds.advanced.findOrCreate)() + const chessF = await p(sbot.metafeeds.advanced.findOrCreate)( rootMF, (f) => f.feedpurpose === 'chess', { @@ -219,12 +219,12 @@ async function setupTree(sbot) { metadata: { score: 0 }, } ) - const indexesMF = await p(sbot.metafeeds.findOrCreate)( + const indexesMF = await p(sbot.metafeeds.advanced.findOrCreate)( rootMF, (f) => f.feedpurpose === 'indexes', { feedpurpose: 'indexes', feedformat: 'bendybutt-v1' } ) - const indexF = await p(sbot.metafeeds.findOrCreate)( + const indexF = await p(sbot.metafeeds.advanced.findOrCreate)( indexesMF, (f) => f.feedpurpose === 'index', { @@ -237,16 +237,16 @@ async function setupTree(sbot) { return { rootMF, chessF, indexesMF, indexF } } -test('findById', (t) => { +test('advanced.findById', (t) => { const sbot = Testbot() setupTree(sbot).then(({ indexF, indexesMF }) => { - sbot.metafeeds.findById(null, (err, details) => { + sbot.metafeeds.advanced.findById(null, (err, details) => { t.match(err.message, /feedId should be provided/, 'error about feedId') t.notOk(details) testReadAndPersisted(t, sbot, (t, sbot, cb) => { - sbot.metafeeds.findById(indexF.keys.id, (err, details) => { + sbot.metafeeds.advanced.findById(indexF.keys.id, (err, details) => { if (err) return cb(err) t.deepEquals(Object.keys(details), [ @@ -303,7 +303,7 @@ test('branchStream', (t) => { }) }) -test('findAndTombstone and tombstoning branchStream', (t) => { +test('advanced.findAndTombstone and tombstoning branchStream', (t) => { const sbot = Testbot() setupTree(sbot).then(({ rootMF }) => { @@ -368,7 +368,7 @@ test('findAndTombstone and tombstoning branchStream', (t) => { }) ) - sbot.metafeeds.findAndTombstone( + sbot.metafeeds.advanced.findAndTombstone( rootMF, (f) => f.feedpurpose === 'chess', 'This game is too good', @@ -379,19 +379,19 @@ test('findAndTombstone and tombstoning branchStream', (t) => { }) }) -test('findOrCreate() recps', (t) => { +test('advanced.findOrCreate (metadata.recps)', (t) => { const sbot = Testbot() - const testkey = Buffer.from( + const ownKey = Buffer.from( '30720d8f9cbf37f6d7062826f6decac93e308060a8aaaa77e6a4747f40ee1a76', 'hex' ) - sbot.box2.setOwnDMKey(testkey) + sbot.box2.setOwnDMKey(ownKey) testReadAndPersisted(t, sbot, (t, sbot, cb) => { - sbot.metafeeds.findOrCreate((err, mf) => { + sbot.metafeeds.advanced.findOrCreate((err, mf) => { if (err) return cb(err) - sbot.metafeeds.findOrCreate( + sbot.metafeeds.advanced.findOrCreate( mf, (f) => f.feedpurpose === 'private', { @@ -411,3 +411,71 @@ test('findOrCreate() recps', (t) => { }) }) }) + +// sugary top level API + +test('findOrCreate', (t) => { + const sbot = Testbot() + + const details = { + feedpurpose: 'chess', + // feedformat: 'classic', optional + } + + sbot.metafeeds.findOrCreate(details, (err, chessF) => { + if (err) throw err + t.equal(chessF.feedpurpose, details.feedpurpose, 'creates feed') + + sbot.metafeeds.findOrCreate(details, (err, chessF2) => { + if (err) throw err + t.deepEqual(chessF, chessF2, 'finds feed') + + pull( + sbot.metafeeds.branchStream({ root: null, old: true, live: false }), + pull.collect((err, branches) => { + if (err) throw err + // console.log(branches.map(branch => branch.map(f => f[1] && f[1].feedpurpose))) + + t.equal(branches.length, 5, 'correct number of feeds created') + // root, v1, shard, chess (AND MAIN) + + const purposePath = branches + .pop() + .map((f) => f[1] && f[1].feedpurpose) + t.deepEqual(purposePath, [null, 'v1', purposePath[2], 'chess']) + // TODO it would be nice for testing that we could deterministically know the shard + // but I don't know how to fix the "seed" that the root feed is derived from + + sbot.close() + t.end() + }) + ) + }) + }) +}) + +test('findOrCreate (metadata.recps)', (t) => { + const sbot = Testbot() + + const ownKey = Buffer.from( + '30720d8f9cbf37f6d7062826f6decac93e308060a8aaaa77e6a4747f40ee1a76', + 'hex' + ) + sbot.box2.setOwnDMKey(ownKey) + + const details = { + feedpurpose: 'chess', + // feedformat: 'classic', optional + metadata: { + recps: [sbot.id], + }, + } + + sbot.metafeeds.findOrCreate(details, (err, chessF) => { + if (err) throw err + + t.deepEqual(chessF.metadata.recps, [sbot.id], 'creates encrypted subfee') + sbot.close() + t.end() + }) +}) diff --git a/test/pick-shard.test.js b/test/pick-shard.test.js new file mode 100644 index 0000000..354af4a --- /dev/null +++ b/test/pick-shard.test.js @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2022 Mix Irving +// +// SPDX-License-Identifier: Unlicense + +const test = require('tape') +const Keys = require('ssb-keys') + +const pickShard = require('../pick-shard') + +test('pick-shard', (t) => { + const rootFeedId = Keys.generate(null, null, 'bendybutt-v1').id + + t.equal( + pickShard(rootFeedId, 'dog'), + pickShard(rootFeedId, 'dog'), + 'is deterministic' + ) + + const validShards = new Set('0123456789abcdef'.split('')) + let pass = true + // NOTE these are all Strings + for (let i = 0; i < 1600; i++) { + const shard = pickShard(rootFeedId, `test-${i}`) + if (!validShards.has(shard)) pass = false + } + t.equal(pass, true, 'picked shards are nibbles') + + t.end() +}) From e1e33b43965e6f8136ce7a6ea00a09d3b4fbc0a4 Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Sep 2022 14:45:10 +1300 Subject: [PATCH 4/8] make isFeed with deepEqual --- .gitignore | 3 ++- api.js | 11 ++++++++--- package.json | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 91e11cd..e174d68 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Unlicense node_modules +package-lock.json pnpm-lock.yaml coverage -.nyc_output \ No newline at end of file +.nyc_output diff --git a/api.js b/api.js index 1b3d009..dc8c8ab 100644 --- a/api.js +++ b/api.js @@ -3,6 +3,7 @@ // SPDX-License-Identifier: LGPL-3.0-only const run = require('promisify-tuple') +const deepEqual = require('fast-deep-equal') const debug = require('debug')('ssb:meta-feeds') const pickShard = require('./pick-shard') @@ -11,9 +12,13 @@ const BB1 = 'bendybutt-v1' const v1Details = { feedpurpose: 'v1', feedformat: BB1 } const v1Visit = detailsToVisit(v1Details) function detailsToVisit(details) { - return (feed) => - feed.feedpurpose === details.feedpurpose && - feed.feedformat === details.feedformat + return (feed) => { + return ( + feed.feedpurpose === details.feedpurpose && + feed.feedformat === details.feedformat && + deepEqual(feed.metadata, details.metadata || {}) + ) + } } exports.init = function (sbot, config) { diff --git a/package.json b/package.json index 1d71164..6a7e376 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "bencode": "^2.0.2", "bipf": "^1.9.0", "debug": "^4.3.0", + "fast-deep-equal": "^3.1.3", "futoin-hkdf": "^1.4.2", "is-canonical-base64": "^1.1.1", "p-defer": "^3.0.0", From dadb9b90089365a6f401e8563685ddfad0cbf0e7 Mon Sep 17 00:00:00 2001 From: mixmix Date: Tue, 27 Sep 2022 14:56:30 +1300 Subject: [PATCH 5/8] Add CC0 header to new README_ADVANCED.md --- README_ADVANCED.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README_ADVANCED.md b/README_ADVANCED.md index 0655b90..f65ca02 100644 --- a/README_ADVANCED.md +++ b/README_ADVANCED.md @@ -1,3 +1,9 @@ + + # Advanced API Most people using this module should not need to access these methods. From a7589f661de3c66e584395603d966e9bba20d976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Staltz?= Date: Tue, 27 Sep 2022 10:56:59 +0300 Subject: [PATCH 6/8] Update api.js --- api.js | 1 + 1 file changed, 1 insertion(+) diff --git a/api.js b/api.js index dc8c8ab..edeefca 100644 --- a/api.js +++ b/api.js @@ -11,6 +11,7 @@ const alwaysTrue = () => true const BB1 = 'bendybutt-v1' const v1Details = { feedpurpose: 'v1', feedformat: BB1 } const v1Visit = detailsToVisit(v1Details) + function detailsToVisit(details) { return (feed) => { return ( From 3bab0d7b302668cbb6909ebf04d423aed7dd0b84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Staltz?= Date: Tue, 27 Sep 2022 10:58:01 +0300 Subject: [PATCH 7/8] Update test/api.test.js --- test/api.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/api.test.js b/test/api.test.js index 65492dd..c1e543f 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -433,7 +433,6 @@ test('findOrCreate', (t) => { sbot.metafeeds.branchStream({ root: null, old: true, live: false }), pull.collect((err, branches) => { if (err) throw err - // console.log(branches.map(branch => branch.map(f => f[1] && f[1].feedpurpose))) t.equal(branches.length, 5, 'correct number of feeds created') // root, v1, shard, chess (AND MAIN) From 49a96c0dcdea6a4c19e3e24cd4ef7877359d3df6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Staltz?= Date: Tue, 27 Sep 2022 11:00:27 +0300 Subject: [PATCH 8/8] Apply suggestions from code review --- test/api.test.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/api.test.js b/test/api.test.js index c1e543f..519ea0f 100644 --- a/test/api.test.js +++ b/test/api.test.js @@ -444,8 +444,7 @@ test('findOrCreate', (t) => { // TODO it would be nice for testing that we could deterministically know the shard // but I don't know how to fix the "seed" that the root feed is derived from - sbot.close() - t.end() + sbot.close(true, t.end) }) ) }) @@ -463,7 +462,6 @@ test('findOrCreate (metadata.recps)', (t) => { const details = { feedpurpose: 'chess', - // feedformat: 'classic', optional metadata: { recps: [sbot.id], }, @@ -473,7 +471,6 @@ test('findOrCreate (metadata.recps)', (t) => { if (err) throw err t.deepEqual(chessF.metadata.recps, [sbot.id], 'creates encrypted subfee') - sbot.close() - t.end() + sbot.close(true, t.end) }) })