From 56ac4f986e01695850b2fcf5b69a410773115dce Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Wed, 13 Sep 2023 14:16:22 +0300 Subject: [PATCH] init --- .github/workflows/node.js.yml | 25 ++++++++++ .gitignore | 6 +++ .prettierrc.yaml | 2 + LICENSE | 20 ++++++++ README.md | 9 ++++ lib/index.js | 42 +++++++++++++++++ package.json | 55 ++++++++++++++++++++++ test/purge-weave.test.js | 86 +++++++++++++++++++++++++++++++++++ test/util.js | 38 ++++++++++++++++ 9 files changed, 283 insertions(+) create mode 100644 .github/workflows/node.js.yml create mode 100644 .gitignore create mode 100644 .prettierrc.yaml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/index.js create mode 100644 package.json create mode 100644 test/purge-weave.test.js create mode 100644 test/util.js diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..d6ab510 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + timeout-minutes: 10 + + strategy: + matrix: + node-version: [16.x, 18.x] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ef31a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.vscode +node_modules +pnpm-lock.yaml +package-lock.json +coverage +*~ diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 0000000..1d2127c --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,2 @@ +semi: false +singleQuote: true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fcb6945 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2023 Andre 'Staltz' Medeiros + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..76f669d --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +**Work in progress** + +## Installation + +We're not on npm yet. In your package.json, include this as + +```js +"ppppp-gc": "github:staltz/ppppp-gc" +``` diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..f353066 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,42 @@ +const makeDebug = require('debug') +const multicb = require('multicb') + +module.exports = { + name: 'gc', + manifest: {}, + permissions: { + anonymous: {}, + }, + init(peer, config) { + if (!peer.goals) throw new Error('gc requires the goals plugin') + const debug = makeDebug('ppppp:gc') + + function purgeGoallessMsgs(cb) { + debug('purge goalless msgs') + const done = multicb() + let waitingForDels = false + for (const rec of peer.db.records()) { + if (!rec.msg) continue + const goals = peer.goals.getByRec(rec) + if (goals.length === 0) { + peer.db.del(rec.id, done()) + waitingForDels = true + } + } + if (waitingForDels) done(cb) + else cb() + } + + function initiate() {} + + function forceImmediately(cb) { + debug('force immediately') + purgeGoallessMsgs(cb) + } + + return { + initiate, + forceImmediately, + } + }, +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2f015c --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "ppppp-gc", + "version": "1.0.0", + "description": "PPPPP garbage collector", + "author": "Andre Staltz ", + "license": "MIT", + "homepage": "https://github.com/staltz/ppppp-gc", + "repository": { + "type": "git", + "url": "git@github.com:staltz/ppppp-gc.git" + }, + "main": "index.js", + "files": [ + "*.js", + "lib/*.js" + ], + "exports": { + ".": { + "require": "./lib/index.js" + } + }, + "type": "commonjs", + "engines": { + "node": ">=16" + }, + "dependencies": { + "debug": "^4.3.4", + "multicb": "^1.2.2" + }, + "devDependencies": { + "bs58": "^5.0.0", + "c8": "7", + "ppppp-caps": "github:staltz/ppppp-caps", + "ppppp-db": "github:staltz/ppppp-db", + "ppppp-goals": "github:staltz/ppppp-goals", + "ppppp-keypair": "github:staltz/ppppp-keypair", + "prettier": "^2.6.2", + "pretty-quick": "^3.1.3", + "rimraf": "^4.4.0", + "secret-handshake-ext": "^0.0.8", + "secret-stack": "~7.1.0", + "ssb-box": "^1.0.1" + }, + "scripts": { + "test": "node --test", + "format-code": "prettier --write \"(lib|test)/**/*.js\"", + "format-code-staged": "pretty-quick --staged --pattern \"(lib|test)/**/*.js\"", + "coverage": "c8 --reporter=lcov npm run test" + }, + "husky": { + "hooks": { + "pre-commit": "npm run format-code-staged" + } + } +} diff --git a/test/purge-weave.test.js b/test/purge-weave.test.js new file mode 100644 index 0000000..3f8dfe6 --- /dev/null +++ b/test/purge-weave.test.js @@ -0,0 +1,86 @@ +const test = require('node:test') +const assert = require('node:assert') +const p = require('node:util').promisify +const Keypair = require('ppppp-keypair') +const { createPeer } = require('./util') + +const bobKeypair = Keypair.generate('ed25519', 'bob') +const carolKeypair = Keypair.generate('ed25519', 'carol') + +function getTexts(msgs) { + return msgs.filter((msg) => msg.data?.text).map((msg) => msg.data.text) +} + +test('purge an orphan weave', async (t) => { + const alice = createPeer({ name: 'alice' }) + + await alice.db.loaded() + + // Alice creates her own account + const aliceID = await p(alice.db.account.create)({ + domain: 'account', + _nonce: 'alice', + }) + // Alice creates Bob + const bobID = await p(alice.db.account.create)({ + domain: 'account', + keypair: bobKeypair, + _nonce: 'bob', + }) + // Alice creates Bob + const carolID = await p(alice.db.account.create)({ + domain: 'account', + keypair: carolKeypair, + _nonce: 'carol', + }) + + const threadRoot = await p(alice.db.feed.publish)({ + account: bobID, + keypair: bobKeypair, + domain: 'post', + data: { text: 'B0' }, + }) + const threadReply1 = await p(alice.db.feed.publish)({ + account: aliceID, + domain: 'post', + data: { text: 'A1' }, + tangles: [threadRoot.id], + }) + const threadReply2 = await p(alice.db.feed.publish)({ + account: carolID, + domain: 'post', + data: { text: 'C1' }, + tangles: [threadRoot.id], + }) + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['B0', 'A1', 'C1'], + 'alice has the full thread' + ) + + await p(alice.db.del)(threadRoot.id) + assert('alice deleted the root') + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A1', 'C1'], + 'alice has only thread replies' + ) + + alice.goals.set(aliceID, 'all') // alice wants her account tangle + alice.goals.set(bobID, 'all') // alice wants bob's account tangle + alice.goals.set(carolID, 'all') // alice wants carol's account tangle + const postFeedID = alice.db.feed.getID(aliceID, 'post') + alice.goals.set(postFeedID, 'all') // alice wants her post feed + + await p(alice.gc.forceImmediately)() + + assert.deepEqual( + getTexts([...alice.db.msgs()]), + ['A1'], + 'alice does not have the thread, except her own reply' + ) + + await p(alice.close)(true) +}) diff --git a/test/util.js b/test/util.js new file mode 100644 index 0000000..51b783a --- /dev/null +++ b/test/util.js @@ -0,0 +1,38 @@ +const os = require('node:os') +const path = require('node:path') +const rimraf = require('rimraf') +const caps = require('ppppp-caps') +const Keypair = require('ppppp-keypair') + +function createPeer(opts) { + if (opts.name) { + opts.path ??= path.join(os.tmpdir(), 'ppppp-gc-' + opts.name) + opts.keypair ??= Keypair.generate('ed25519', opts.name) + opts.name = undefined + } + if (!opts.path) throw new Error('need opts.path in createPeer()') + if (!opts.keypair) throw new Error('need opts.keypair in createPeer()') + + rimraf.sync(opts.path) + return require('secret-stack/bare')() + .use(require('secret-stack/plugins/net')) + .use(require('secret-handshake-ext/secret-stack')) + .use(require('ppppp-db')) + .use(require('ppppp-goals')) + .use(require('ssb-box')) + .use(require('../lib')) + .call(null, { + caps, + connections: { + incoming: { + net: [{ scope: 'device', transform: 'shse', port: null }], + }, + outgoing: { + net: [{ transform: 'shse' }], + }, + }, + ...opts, + }) +} + +module.exports = { createPeer }