Rename to pzp

This commit is contained in:
Jacob Karlsson 2024-04-30 16:37:37 +02:00
parent 46a8d8889c
commit 94b3ca3a3f
7 changed files with 152 additions and 135 deletions

View File

@ -1,25 +0,0 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
node-version: [18.x, 20.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

13
.woodpecker.yaml Normal file
View File

@ -0,0 +1,13 @@
matrix:
NODE_VERSION:
- 18
- 20
steps:
test:
when:
event: [push]
image: node:${NODE_VERSION}
commands:
- npm install
- npm test

View File

@ -1,9 +1,7 @@
**Work in progress**
# pzp-goals
## Installation
We're not on npm yet. In your package.json, include this as
```js
"ppppp-goals": "github:staltz/ppppp-goals"
```
npm install pzp-goals
```

View File

@ -1,14 +1,24 @@
// @ts-ignore
const Obz = require('obz')
// @ts-ignore
const p = (fn) => (...args) => {
return new Promise((res, rej) => {
// @ts-ignore
fn(...args, (err, val) => {
if (err) return rej(err)
return res(val)
})
})
}
/**
* @typedef {ReturnType<import('ppppp-db').init>} PPPPPDB
* @typedef {ReturnType<import('ppppp-dict').init>} PPPPPDict
* @typedef {ReturnType<import('ppppp-set').init>} PPPPPSet
* @typedef {import('ppppp-db').RecPresent} RecPresent
* @typedef {import('ppppp-db').Tangle} Tangle
* @typedef {import('ppppp-db').Msg} Msg
* @typedef {NonNullable<ReturnType<PPPPPDB['getTangle']>>} DBTangle
* @typedef {ReturnType<import('pzp-db').init>} PZPDB
* @typedef {ReturnType<import('pzp-dict').init>} PZPDict
* @typedef {ReturnType<import('pzp-set').init>} PZPSet
* @typedef {import('pzp-db').RecPresent} RecPresent
* @typedef {import('pzp-db').Tangle} Tangle
* @typedef {import('pzp-db').Msg} Msg
* @typedef {NonNullable<ReturnType<PZPDB['getTangle']>>} DBTangle
* @typedef {string} MsgID
* @typedef {'none'|'all'|`newest-${number}`|'dict'|'set'} GoalDSL
* @typedef {'none'|'all'|'newest'|'dict'|'set'} GoalType
@ -19,7 +29,10 @@ const Obz = require('obz')
/**
* @template T
* @typedef {(...args: [Error] | [null, T]) => void } CB
* @typedef {[T] extends [void] ?
* (...args: [Error] | []) => void :
* (...args: [Error] | [null, T]) => void
* } CB
*/
/**
@ -117,7 +130,7 @@ class GoalImpl {
}
/**
* @param {{ db: PPPPPDB, dict: PPPPPDict, set: PPPPPSet }} peer
* @param {{ db: PZPDB, dict: PZPDict, set: PZPSet }} peer
* @param {unknown} config
*/
function initGoals(peer, config) {
@ -142,31 +155,35 @@ function initGoals(peer, config) {
* @private
* @param {Goal} goal
* @param {Tangle} tangle
* @returns {Range}
* @param {CB<Range>} cb
*/
function crossGoalWithTangle(goal, tangle) {
function crossGoalWithTangle(goal, tangle, cb) {
const maxDepth = tangle.maxDepth
switch (goal.type) {
case 'newest':
const start = Math.max(0, maxDepth - goal.count + 1)
return [start, maxDepth]
return cb(null, [start, maxDepth])
case 'all':
return [0, maxDepth]
return cb(null, [0, maxDepth])
case 'set':
const minSetDepth = peer.set.minRequiredDepth(goal.id)
return [minSetDepth, maxDepth]
return peer.set.minRequiredDepth(goal.id, (err, minSetDepth) => {
if (err) return cb(err)
return cb(null, [minSetDepth, maxDepth])
})
case 'dict':
const minDictDepth = peer.dict.minRequiredDepth(goal.id)
return [minDictDepth, maxDepth]
return peer.dict.minRequiredDepth(goal.id, (err, minDictDepth) => {
if (err) return cb(err)
return cb(null, [minDictDepth, maxDepth])
})
case 'none':
return EMPTY_RANGE
return cb(null, EMPTY_RANGE)
default:
throw new Error(`Unrecognized goal type: ${goal.type}`)
return cb(Error(`Unrecognized goal type: ${goal.type}`))
}
}
@ -225,73 +242,87 @@ function initGoals(peer, config) {
* @public
* @param {MsgID} msgID
* @param {Msg} msg
* @returns {PurposeWithDetails}
* @param {CB<PurposeWithDetails>} cb
*/
function getMsgPurpose(msgID, msg) {
function getMsgPurpose(msgID, msg, cb) {
let servesAsTrail = false
// Check whether this msg is a goalful root of some tangle:
asRoot: if (goals.has(msgID)) {
const goal = /** @type {GoalImpl} */ (goals.get(msgID))
if (goal.type === 'none') break asRoot
const tangle = peer.db.getTangle(msgID)
if (!tangle) break asRoot
const range = crossGoalWithTangle(goal, tangle)
if (isEmptyRange(range)) break asRoot
const [min] = range
if (min === 0) return ['goal']
if (min > 0) servesAsTrail = true
}
/** @return {Promise<PurposeWithDetails>} */
async function getPurpose() {
// Check whether this msg is a goalful root of some tangle:
asRoot: if (goals.has(msgID)) {
const goal = /** @type {GoalImpl} */ (goals.get(msgID))
if (goal.type === 'none') break asRoot
const tangle = await p(peer.db.getTangle)(msgID)
if (!tangle) break asRoot
const range = await p(crossGoalWithTangle)(goal, tangle)
if (isEmptyRange(range)) break asRoot
const [min] = range
if (min === 0) return ['goal']
if (min > 0) servesAsTrail = true
}
// Check whether this msg is a goalful affix of some tangle:
const validTangles =
/** @type {Array<[DBTangle, number, number, number, GoalType]>} */ ([])
asAffix: for (const tangleID in msg.metadata.tangles) {
if (!goals.has(tangleID)) continue asAffix
const goal = /** @type {GoalImpl} */ (goals.get(tangleID))
if (goal.type === 'none') continue asAffix
const tangle = peer.db.getTangle(tangleID)
if (!tangle) continue asAffix
const [min, max] = crossGoalWithTangle(goal, tangle)
if (min > max) continue asAffix
const recDepth = tangle.getDepth(msgID)
if (recDepth < 0) continue asAffix
validTangles.push([tangle, min, max, recDepth, goal.type])
}
// (Loop over once without heavy computations and maybe return early:)
for (const [, min, max, recDepth] of validTangles) {
if (min <= recDepth && recDepth <= max) return ['goal']
}
// At this point we know that the msg *cannot* serve as 'goal',
// so if it serves as trail, that'll do:
if (servesAsTrail) return ['trail']
// Check whether this msg is a trail affix of some tangle:
// (Loop again with heavy computations now that it's inevitable:)
for (const [tangle, min] of validTangles) {
const minMsgIDs = tangle
.topoSort()
.filter((msgID) => tangle.getDepth(msgID) === min)
const { erasables } = tangle.getDeletablesAndErasables(...minMsgIDs)
if (erasables.has(msgID)) return ['trail']
}
// Check whether this msg is a goalful affix of some tangle:
const validTangles =
/** @type {Array<[DBTangle, number, number, number, GoalType]>} */ ([])
asAffix: for (const tangleID in msg.metadata.tangles) {
if (!goals.has(tangleID)) continue asAffix
const goal = /** @type {GoalImpl} */ (goals.get(tangleID))
if (goal.type === 'none') continue asAffix
const tangle = await p(peer.db.getTangle)(tangleID)
if (!tangle) continue asAffix
const [min, max] = await p(crossGoalWithTangle)(goal, tangle)
if (min > max) continue asAffix
const recDepth = tangle.getDepth(msgID)
if (recDepth < 0) continue asAffix
validTangles.push([tangle, min, max, recDepth, goal.type])
}
// (Loop over once without heavy computations and maybe return early:)
for (const [, min, max, recDepth] of validTangles) {
if (min <= recDepth && recDepth <= max) return ['goal']
}
// At this point we know that the msg *cannot* serve as 'goal',
// so if it serves as trail, that'll do:
if (servesAsTrail) return ['trail']
// Check whether this msg is a trail affix of some tangle:
// (Loop again with heavy computations now that it's inevitable:)
for (const [ tangle, min] of validTangles) {
const minMsgIDs = tangle
// @ts-ignore
.topoSort()
// @ts-ignore
.filter((msgID) => tangle.getDepth(msgID) === min)
// @ts-ignore
const { erasables } = tangle.getDeletablesAndErasables(...minMsgIDs)
if (erasables.has(msgID)) return ['trail']
}
// Check whether this msg is a ghost affix of some tangle:
for (const [tangle, min, max, recDepth, goalType] of validTangles) {
if (goalType === 'dict') {
const span = peer.dict.getGhostSpan()
if (peer.dict.isGhostable(msgID, tangle.id)) {
return ['ghost', { tangleID: tangle.id, span }]
}
}
if (goalType === 'set') {
const span = peer.set.getGhostSpan()
if (peer.set.isGhostable(msgID, tangle.id)) {
return ['ghost', { tangleID: tangle.id, span }]
// Check whether this msg is a ghost affix of some tangle:
for (const [tangle, min, max, recDepth, goalType] of validTangles) {
if (goalType === 'dict') {
const span = peer.dict.getGhostSpan()
// @ts-ignore
if (await p(peer.dict.isGhostable)(msgID, tangle.id)) {
// @ts-ignore
return ['ghost', { tangleID: tangle.id, span }]
}
}
if (goalType === 'set') {
const span = peer.set.getGhostSpan()
// @ts-ignore
if (await p(peer.set.isGhostable)(msgID, tangle.id)) {
// @ts-ignore
return ['ghost', { tangleID: tangle.id, span }]
}
}
}
return ['none']
}
return ['none']
getPurpose()
.then(purpose => cb(null, purpose))
.catch(err => cb(err))
}
/**

View File

@ -1,13 +1,13 @@
{
"name": "ppppp-goals",
"version": "1.0.0",
"description": "PPPPP tracker of replication goals",
"name": "pzp-goals",
"version": "0.0.1",
"description": "PZP tracker of replication goals",
"author": "Andre Staltz <contact@staltz.com>",
"license": "MIT",
"homepage": "https://github.com/staltz/ppppp-goals",
"homepage": "https://codeberg.org/pzp/pzp-goals",
"repository": {
"type": "git",
"url": "git@github.com:staltz/ppppp-goals.git"
"url": "git@codeberg.org:pzp/pzp-goals.git"
},
"main": "index.js",
"files": [
@ -32,11 +32,11 @@
"devDependencies": {
"bs58": "^5.0.0",
"c8": "7",
"ppppp-caps": "github:staltz/ppppp-caps#93fa810b9a40b78aef4872d4c2a8412cccb52929",
"ppppp-db": "github:staltz/ppppp-db#667b33779d98aff12a9b0cd2d7c80469a95cd04e",
"ppppp-dict": "github:staltz/ppppp-dict#6f0ff4e3383a8c18b766949f6db9b51460ecb640",
"ppppp-keypair": "github:staltz/ppppp-keypair#61ef4420578f450dc2cc7b1efc1c5a691a871c74",
"ppppp-set": "github:staltz/ppppp-set#8983ba29f03db95a76b4bd9a55aa4392b350fdbb",
"pzp-caps": "^1.0.0",
"pzp-db": "^1.0.1",
"pzp-dict": "^1.0.0",
"pzp-keypair": "^1.0.0",
"pzp-set": "^1.0.0",
"prettier": "^2.6.2",
"pretty-quick": "^3.1.3",
"rimraf": "^4.4.0",

View File

@ -31,7 +31,7 @@ test('set, getByID, list, watch', async (t) => {
subdomain: 'account',
_nonce: 'alice',
})
const aliceAccountRoot = alice.db.getRecord(aliceID)
const aliceAccountRoot = await p(alice.db.getRecord)(aliceID)
const watched = []
const stopListening = alice.goals.watch((goal) => {
@ -53,7 +53,7 @@ test('set, getByID, list, watch', async (t) => {
}
{
const [purpose] = alice.goals.getMsgPurpose(
const [purpose] = await p(alice.goals.getMsgPurpose)(
aliceAccountRoot.id,
aliceAccountRoot.msg
)
@ -113,12 +113,12 @@ test('getMsgPurpose', async (t) => {
const gottenGoal = alice.goals.get(feedID)
assert.strictEqual(gottenGoal.id, feedID, 'gotten goal id is correct')
const [purpose] = alice.goals.getMsgPurpose(post2.id, post2.msg)
const [purpose] = await p(alice.goals.getMsgPurpose)(post2.id, post2.msg)
assert.equal(purpose, 'goal', 'purpose is "goal"')
alice.goals.set(feedID, 'newest-1')
assert('set goal to newest-1')
const [purpose2] = alice.goals.getMsgPurpose(post2.id, post2.msg)
const [purpose2] = await p(alice.goals.getMsgPurpose)(post2.id, post2.msg)
assert.equal(purpose2, 'none', 'purpose2 is "none"')
await p(alice.close)(true)
@ -141,20 +141,20 @@ test('getMsgPurpose ghost', async (t) => {
await p(alice.dict.update)('profile', { name: 'ALICIAA' })
const feedID = alice.dict.getFeedID('profile')
const tangle = alice.db.getTangle(feedID)
const tangle = await p(alice.db.getTangle)(feedID)
const msgIDs = tangle.topoSort()
assert.equal(msgIDs.length, 6, 'tangle has root+5 messages')
const recs = msgIDs.map((id) => alice.db.getRecord(id))
const recs = await Promise.all(msgIDs.map((id) =>p(alice.db.getRecord)(id)))
alice.goals.set(feedID, 'dict')
assert.equal(alice.goals.getMsgPurpose(recs[1].id, recs[1].msg)[0], 'none')
assert.equal(alice.goals.getMsgPurpose(recs[2].id, recs[2].msg)[0], 'ghost')
assert.equal(alice.goals.getMsgPurpose(recs[3].id, recs[3].msg)[0], 'trail')
assert.equal(alice.goals.getMsgPurpose(recs[4].id, recs[4].msg)[0], 'trail')
assert.equal(alice.goals.getMsgPurpose(recs[5].id, recs[5].msg)[0], 'goal')
assert.equal((await p(alice.goals.getMsgPurpose)(recs[1].id, recs[1].msg))[0], 'none')
assert.equal((await p(alice.goals.getMsgPurpose)(recs[2].id, recs[2].msg))[0], 'ghost')
assert.equal((await p(alice.goals.getMsgPurpose)(recs[3].id, recs[3].msg))[0], 'trail')
assert.equal((await p(alice.goals.getMsgPurpose)(recs[4].id, recs[4].msg))[0], 'trail')
assert.equal((await p(alice.goals.getMsgPurpose)(recs[5].id, recs[5].msg))[0], 'goal')
const [purpose, details] = alice.goals.getMsgPurpose(recs[2].id, recs[2].msg)
const [purpose, details] = await p(alice.goals.getMsgPurpose)(recs[2].id, recs[2].msg)
assert.equal(purpose, 'ghost')
assert.deepEqual(details, { tangleID: feedID, span: 3 })

View File

@ -1,15 +1,15 @@
const OS = require('node:os')
const Path = require('node:path')
const rimraf = require('rimraf')
const caps = require('ppppp-caps')
const Keypair = require('ppppp-keypair')
const caps = require('pzp-caps')
const Keypair = require('pzp-keypair')
function createPeer(config) {
if (config.name) {
const name = config.name
const tmp = OS.tmpdir()
config.global ??= {}
config.global.path ??= Path.join(tmp, `ppppp-goals-${name}-${Date.now()}`)
config.global.path ??= Path.join(tmp, `pzp-goals-${name}-${Date.now()}`)
config.global.keypair ??= Keypair.generate('ed25519', name)
delete config.name
}
@ -27,9 +27,9 @@ function createPeer(config) {
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-dict'))
.use(require('ppppp-set'))
.use(require('pzp-db'))
.use(require('pzp-dict'))
.use(require('pzp-set'))
.use(require('ssb-box'))
.use(require('../lib'))
.call(null, {