update to ppppp-db with MsgV3

This commit is contained in:
Andre Staltz 2023-09-13 19:04:12 +03:00
parent f31d700a3b
commit b3a96dbb6d
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
5 changed files with 258 additions and 212 deletions

View File

@ -1,77 +1,71 @@
const FeedV1 = require('ppppp-db/feed-v1') const MsgV3 = require('ppppp-db/msg-v3')
const PREFIX = 'record_v1__' const PREFIX = 'record_v1__'
/** @typedef {string} Subtype */ /**
* @typedef {string} Subdomain
/** @typedef {string} MsgHash */ * @typedef {string} MsgID
* @typedef {`${Subdomain}.${string}`} SubdomainField
/** @typedef {`${Subtype}.${string}`} SubtypeField */ */
/** /**
* @param {string} type * @param {string} domain
* @returns {Subtype} * @returns {Subdomain}
*/ */
function toSubtype(type) { function toSubdomain(domain) {
return type.slice(PREFIX.length) return domain.slice(PREFIX.length)
} }
/** /**
* @param {Subtype} subtype * @param {Subdomain} subdomain
* @returns {string} * @returns {string}
*/ */
function fromSubtype(subtype) { function fromSubdomain(subdomain) {
return PREFIX + subtype return PREFIX + subdomain
} }
module.exports = { module.exports = {
name: 'record', name: 'record',
manifest: { manifest: {},
update: 'async',
get: 'sync',
getFieldRoots: 'sync',
squeeze: 'async',
},
init(peer, config) { init(peer, config) {
//#region state //#region state
const myWho = FeedV1.stripAuthor(config.keys.id) let accountID = /** @type {string | null} */ (null)
let cancelListeningToRecordAdded = null let cancelListeningToRecordAdded = null
let loadPromise = /** @type {Promise<void> | null} */ (null)
/** @type {Map<Subtype, unknown>} */ const tangles = /** @type {Map<Subdomain, unknown>} */ (new Map())
const tangles = new Map()
const fieldRoots = { const fieldRoots = {
/** @type {Map<SubtypeField, Set<MsgHash>} */ /** @type {Map<SubdomainField, Set<MsgID>} */
_map: new Map(), _map: new Map(),
_getKey(subtype, field) { _getKey(subdomain, field) {
return subtype + '.' + field return subdomain + '.' + field
}, },
get(subtype, field = null) { get(subdomain, field = null) {
if (field) { if (field) {
const key = this._getKey(subtype, field) const key = this._getKey(subdomain, field)
return this._map.get(key) return this._map.get(key)
} else { } else {
const out = {} const out = {}
for (const [key, value] of this._map.entries()) { for (const [key, value] of this._map.entries()) {
if (key.startsWith(subtype + '.')) { if (key.startsWith(subdomain + '.')) {
const field = key.slice(subtype.length + 1) const field = key.slice(subdomain.length + 1)
out[field] = [...value] out[field] = [...value]
} }
} }
return out return out
} }
}, },
add(subtype, field, msgHash) { add(subdomain, field, msgID) {
const key = this._getKey(subtype, field) const key = this._getKey(subdomain, field)
const set = this._map.get(key) ?? new Set() const set = this._map.get(key) ?? new Set()
set.add(msgHash) set.add(msgID)
return this._map.set(key, set) return this._map.set(key, set)
}, },
del(subtype, field, msgHash) { del(subdomain, field, msgID) {
const key = this._getKey(subtype, field) const key = this._getKey(subdomain, field)
const set = this._map.get(key) const set = this._map.get(key)
if (!set) return false if (!set) return false
set.delete(msgHash) set.delete(msgID)
if (set.size === 0) this._map.delete(key) if (set.size === 0) this._map.delete(key)
return true return true
}, },
@ -82,16 +76,6 @@ module.exports = {
//#endregion //#endregion
//#region active processes //#region active processes
const loadPromise = new Promise((resolve, reject) => {
for (const { hash, msg } of peer.db.records()) {
maybeLearnAboutRecord(hash, msg)
}
cancelListeningToRecordAdded = peer.db.onRecordAdded(({ hash, msg }) => {
maybeLearnAboutRecord(hash, msg)
})
resolve()
})
peer.close.hook(function (fn, args) { peer.close.hook(function (fn, args) {
cancelListeningToRecordAdded() cancelListeningToRecordAdded()
fn.apply(this, args) fn.apply(this, args)
@ -101,65 +85,64 @@ module.exports = {
//#region internal methods //#region internal methods
function isValidRecordRootMsg(msg) { function isValidRecordRootMsg(msg) {
if (!msg) return false if (!msg) return false
if (msg.metadata.who !== myWho) return false if (msg.metadata.account !== accountID) return false
const type = msg.metadata.type const domain = msg.metadata.domain
if (!type.startsWith(PREFIX)) return false if (!domain.startsWith(PREFIX)) return false
return FeedV1.isFeedRoot(msg, config.keys.id, type) return MsgV3.isMoot(msg, accountID, domain)
} }
function isValidRecordMsg(msg) { function isValidRecordMsg(msg) {
if (!msg) return false if (!msg) return false
if (!msg.content) return false if (!msg.data) return false
if (msg.metadata.who !== myWho) return false if (msg.metadata.account !== accountID) return false
if (!msg.metadata.type.startsWith(PREFIX)) return false if (!msg.metadata.domain.startsWith(PREFIX)) return false
if (!msg.content.update) return false if (!msg.data.update) return false
if (typeof msg.content.update !== 'object') return false if (typeof msg.data.update !== 'object') return false
if (Array.isArray(msg.content.update)) return false if (Array.isArray(msg.data.update)) return false
if (!Array.isArray(msg.content.supersedes)) return false if (!Array.isArray(msg.data.supersedes)) return false
return true return true
} }
function learnRecordRoot(hash, msg) { function learnRecordRoot(rootID, root) {
const { type } = msg.metadata const subdomain = toSubdomain(root.metadata.domain)
const subtype = toSubtype(type) const tangle = tangles.get(subdomain) ?? new MsgV3.Tangle(rootID)
const tangle = tangles.get(subtype) ?? new FeedV1.Tangle(hash) tangle.add(rootID, root)
tangle.add(hash, msg) tangles.set(subdomain, tangle)
tangles.set(subtype, tangle)
} }
function learnRecordUpdate(hash, msg) { function learnRecordUpdate(msgID, msg) {
const { who, type } = msg.metadata const { account, domain } = msg.metadata
const rootHash = FeedV1.getFeedRootHash(who, type) const rootID = MsgV3.getMootID(account, domain)
const subtype = toSubtype(type) const subdomain = toSubdomain(domain)
const tangle = tangles.get(subtype) ?? new FeedV1.Tangle(rootHash) const tangle = tangles.get(subdomain) ?? new MsgV3.Tangle(rootID)
tangle.add(hash, msg) tangle.add(msgID, msg)
tangles.set(subtype, tangle) tangles.set(subdomain, tangle)
for (const field in msg.content.update) { for (const field in msg.data.update) {
const existing = fieldRoots.get(subtype, field) const existing = fieldRoots.get(subdomain, field)
if (!existing) { if (!existing) {
fieldRoots.add(subtype, field, hash) fieldRoots.add(subdomain, field, msgID)
} else { } else {
for (const existingHash of existing) { for (const existingID of existing) {
if (tangle.precedes(existingHash, hash)) { if (tangle.precedes(existingID, msgID)) {
fieldRoots.del(subtype, field, existingHash) fieldRoots.del(subdomain, field, existingID)
fieldRoots.add(subtype, field, hash) fieldRoots.add(subdomain, field, msgID)
} else { } else {
fieldRoots.add(subtype, field, hash) fieldRoots.add(subdomain, field, msgID)
} }
} }
} }
} }
} }
function maybeLearnAboutRecord(hash, msg) { function maybeLearnAboutRecord(msgID, msg) {
if (msg.metadata.who !== myWho) return if (msg.metadata.account !== accountID) return
if (isValidRecordRootMsg(msg)) { if (isValidRecordRootMsg(msg)) {
learnRecordRoot(hash, msg) learnRecordRoot(msgID, msg)
return return
} }
if (isValidRecordMsg(msg)) { if (isValidRecordMsg(msg)) {
learnRecordUpdate(hash, msg) learnRecordUpdate(msgID, msg)
return return
} }
} }
@ -169,73 +152,92 @@ module.exports = {
else loadPromise.then(() => cb(null), cb) else loadPromise.then(() => cb(null), cb)
} }
function _squeezePotential(subtype) { function _squeezePotential(subdomain) {
// TODO: improve this so that the squeezePotential is the size of the // TODO: improve this so that the squeezePotential is the size of the
// tangle suffix built as a slice from the fieldRoots // tangle suffix built as a slice from the fieldRoots
const rootHash = FeedV1.getFeedRootHash(myWho, fromSubtype(subtype)) const mootID = MsgV3.getMootID(accountID, fromSubdomain(subdomain))
const tangle = peer.db.getTangle(rootHash) const tangle = peer.db.getTangle(mootID)
const maxDepth = tangle.getMaxDepth() const maxDepth = tangle.maxDepth
const fieldRoots = getFieldRoots(myWho, subtype) const fieldRoots = getFieldRoots(accountID, subdomain)
let minDepth = Infinity let minDepth = Infinity
for (const field in fieldRoots) { for (const field in fieldRoots) {
for (const msgHash of fieldRoots[field]) { for (const msgID of fieldRoots[field]) {
const depth = tangle.getDepth(msgHash) const depth = tangle.getDepth(msgID)
if (depth < minDepth) minDepth = depth if (depth < minDepth) minDepth = depth
} }
} }
return maxDepth - minDepth return maxDepth - minDepth
} }
function forceUpdate(subtype, update, cb) { function forceUpdate(subdomain, update, cb) {
const type = fromSubtype(subtype) const domain = fromSubdomain(subdomain)
// Populate supersedes // Populate supersedes
const supersedes = [] const supersedes = []
for (const field in update) { for (const field in update) {
const existing = fieldRoots.get(subtype, field) const existing = fieldRoots.get(subdomain, field)
if (existing) supersedes.push(...existing) if (existing) supersedes.push(...existing)
} }
peer.db.create({ type, content: { update, supersedes } }, (err, rec) => { peer.db.feed.publish(
{ account: accountID, domain, data: { update, supersedes } },
(err, rec) => {
// prettier-ignore // prettier-ignore
if (err) return cb(new Error('Failed to create msg when force updating Record', { cause: err })) if (err) return cb(new Error('Failed to create msg when force updating Record', { cause: err }))
cb(null, true) cb(null, true)
}) }
)
} }
//#endregion //#endregion
//#region public methods //#region public methods
function getFieldRoots(authorId, subtype) {
const who = FeedV1.stripAuthor(authorId) /**
// prettier-ignore * @param {string} id
if (who !== myWho) return cb(new Error('Cannot getFieldRoots for another user\'s record. Given "authorId" was ' + authorId)) */
return fieldRoots.get(subtype) function load(id, cb) {
accountID = id
loadPromise = new Promise((resolve, reject) => {
for (const { id, msg } of peer.db.records()) {
maybeLearnAboutRecord(id, msg)
}
cancelListeningToRecordAdded = peer.db.onRecordAdded(({ id, msg }) => {
maybeLearnAboutRecord(id, msg)
})
resolve()
cb()
})
} }
function get(authorId, subtype) { function getFieldRoots(id, subdomain) {
const type = fromSubtype(subtype) // prettier-ignore
const rootHash = FeedV1.getFeedRootHash(authorId, type) if (id !== accountID) return cb(new Error(`Cannot getFieldRoots for another user's record. Given ID was "${id}"`))
const tangle = peer.db.getTangle(rootHash) return fieldRoots.get(subdomain)
if (!tangle || tangle.size() === 0) return {} }
const msgHashes = tangle.topoSort()
function get(id, subdomain) {
const domain = fromSubdomain(subdomain)
const mootID = MsgV3.getMootID(id, domain)
const tangle = peer.db.getTangle(mootID)
if (!tangle || tangle.size === 0) return {}
const msgIDs = tangle.topoSort()
const record = {} const record = {}
for (const msgHash of msgHashes) { for (const msgID of msgIDs) {
const msg = peer.db.get(msgHash) const msg = peer.db.get(msgID)
if (isValidRecordMsg(msg)) { if (isValidRecordMsg(msg)) {
const { update } = msg.content const { update } = msg.data
Object.assign(record, update) Object.assign(record, update)
} }
} }
return record return record
} }
function update(authorId, subtype, update, cb) { function update(id, subdomain, update, cb) {
const who = FeedV1.stripAuthor(authorId)
// prettier-ignore // prettier-ignore
if (who !== myWho) return cb(new Error('Cannot update another user\'s record. Given "authorId" was ' + authorId)) if (id !== accountID) return cb(new Error(`Cannot update another user's record. Given ID was "${id}"`))
loaded(() => { loaded(() => {
const record = get(authorId, subtype) const record = get(id, subdomain)
let hasChanges = false let hasChanges = false
for (const [field, value] of Object.entries(update)) { for (const [field, value] of Object.entries(update)) {
@ -245,20 +247,19 @@ module.exports = {
} }
} }
if (!hasChanges) return cb(null, false) if (!hasChanges) return cb(null, false)
forceUpdate(subtype, update, cb) forceUpdate(subdomain, update, cb)
}) })
} }
function squeeze(authorId, subtype, cb) { function squeeze(id, subdomain, cb) {
const who = FeedV1.stripAuthor(authorId)
// prettier-ignore // prettier-ignore
if (who !== myWho) return cb(new Error('Cannot squeeze another user\'s record. Given "authorId" was ' + authorId)) if (id !== accountID) return cb(new Error(`Cannot squeeze another user's record. Given ID was "${id}"`))
const potential = _squeezePotential(subtype) const potential = _squeezePotential(subdomain)
if (potential < 1) return cb(null, false) if (potential < 1) return cb(null, false)
loaded(() => { loaded(() => {
const record = get(authorId, subtype) const record = get(id, subdomain)
forceUpdate(subtype, record, (err) => { forceUpdate(subdomain, record, (err) => {
// prettier-ignore // prettier-ignore
if (err) return cb(new Error('Failed to force update when squeezing Record', { cause: err })) if (err) return cb(new Error('Failed to force update when squeezing Record', { cause: err }))
cb(null, true) cb(null, true)
@ -268,6 +269,7 @@ module.exports = {
//#endregion //#endregion
return { return {
load,
update, update,
get, get,
getFieldRoots, getFieldRoots,

View File

@ -28,19 +28,20 @@
"devDependencies": { "devDependencies": {
"bs58": "^5.0.0", "bs58": "^5.0.0",
"c8": "7", "c8": "7",
"ppppp-db": "github:staltz/ppppp-db#rev1", "ppppp-db": "github:staltz/ppppp-db",
"ppppp-caps": "github:staltz/ppppp-caps",
"ppppp-keypair": "github:staltz/ppppp-keypair",
"rimraf": "^4.4.0", "rimraf": "^4.4.0",
"secret-stack": "^6.4.1", "secret-stack": "~7.1.0",
"secret-handshake-ext": "^0.0.8",
"ssb-box": "^1.0.1", "ssb-box": "^1.0.1",
"ssb-caps": "^1.1.0", "typescript": "^5.1.3"
"ssb-classic": "^1.1.0",
"ssb-keys": "^8.5.0",
"ssb-uri2": "^2.4.1",
"tap-arc": "^0.3.5",
"tape": "^5.6.3"
}, },
"scripts": { "scripts": {
"test": "tape test/*.js | tap-arc --bail", "clean-check": "tsc --build --clean",
"prepublishOnly": "npm run clean-check && tsc --build",
"postpublish": "npm run clean-check",
"test": "npm run clean-check && node --test",
"format-code": "prettier --write \"(lib|test)/**/*.js\"", "format-code": "prettier --write \"(lib|test)/**/*.js\"",
"format-code-staged": "pretty-quick --staged --pattern \"(lib|test)/**/*.js\"", "format-code-staged": "pretty-quick --staged --pattern \"(lib|test)/**/*.js\"",
"coverage": "c8 --reporter=lcov npm run test" "coverage": "c8 --reporter=lcov npm run test"

View File

@ -1,145 +1,149 @@
const test = require('tape') const test = require('node:test')
const assert = require('node:assert')
const path = require('path') const path = require('path')
const os = require('os') const os = require('os')
const rimraf = require('rimraf') const rimraf = require('rimraf')
const SecretStack = require('secret-stack') const MsgV3 = require('ppppp-db/msg-v3')
const FeedV1 = require('ppppp-db/feed-v1') const Keypair = require('ppppp-keypair')
const caps = require('ssb-caps')
const p = require('util').promisify const p = require('util').promisify
const { generateKeypair } = require('./util') const { createPeer } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-record') const DIR = path.join(os.tmpdir(), 'ppppp-record')
rimraf.sync(DIR) rimraf.sync(DIR)
const aliceKeys = generateKeypair('alice') const aliceKeypair = Keypair.generate('ed25519', 'alice')
const who = aliceKeys.id
let peer let peer
let aliceID
test('setup', async (t) => { test('setup', async (t) => {
peer = SecretStack({ appKey: caps.shs }) peer = createPeer({ keypair: aliceKeypair, path: DIR })
.use(require('ppppp-db'))
.use(require('ssb-box'))
.use(require('../lib'))
.call(null, {
keys: aliceKeys,
path: DIR,
})
await peer.db.loaded() await peer.db.loaded()
aliceID = await p(peer.db.account.create)({
domain: 'account',
_nonce: 'alice',
})
await p(peer.record.load)(aliceID)
}) })
test('Record update() and get()', async (t) => { test('Record update() and get()', async (t) => {
t.ok( assert(
await p(peer.record.update)(who, 'profile', { name: 'alice' }), await p(peer.record.update)(aliceID, 'profile', { name: 'alice' }),
'update .name' 'update .name'
) )
t.deepEqual(peer.record.get(who, 'profile'), { name: 'alice' }, 'get') assert.deepEqual(peer.record.get(aliceID, 'profile'), { name: 'alice' }, 'get')
const fieldRoots1 = peer.record.getFieldRoots(who, 'profile') const fieldRoots1 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals(fieldRoots1, { name: ['Pt4YwxksvCLir45Tmw3hXK'] }, 'fieldRoots') assert.deepEqual(fieldRoots1, { name: ['PbwnLbJS4oninQ1RPCdgRn'] }, 'fieldRoots')
t.ok(await p(peer.record.update)(who, 'profile', { age: 20 }), 'update .age') assert(await p(peer.record.update)(aliceID, 'profile', { age: 20 }), 'update .age')
t.deepEqual( assert.deepEqual(
peer.record.get(who, 'profile'), peer.record.get(aliceID, 'profile'),
{ name: 'alice', age: 20 }, { name: 'alice', age: 20 },
'get' 'get'
) )
const fieldRoots2 = peer.record.getFieldRoots(who, 'profile') const fieldRoots2 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals( assert.deepEqual(
fieldRoots2, fieldRoots2,
{ name: ['Pt4YwxksvCLir45Tmw3hXK'], age: ['XqkG9Uz1eQcxv9R1f3jgKS'] }, { name: ['PbwnLbJS4oninQ1RPCdgRn'], age: ['9iTTqNabtnXmw4AiZxNMRq'] },
'fieldRoots' 'fieldRoots'
) )
t.false( assert.equal(
await p(peer.record.update)(who, 'profile', { name: 'alice' }), await p(peer.record.update)(aliceID, 'profile', { name: 'alice' }),
false,
'redundant update .name' 'redundant update .name'
) )
t.deepEqual( assert.deepEqual(
peer.record.get(who, 'profile'), peer.record.get(aliceID, 'profile'),
{ name: 'alice', age: 20 }, { name: 'alice', age: 20 },
'get' 'get'
) )
t.true( assert.equal(
await p(peer.record.update)(who, 'profile', { name: 'Alice' }), await p(peer.record.update)(aliceID, 'profile', { name: 'Alice' }),
true,
'update .name' 'update .name'
) )
t.deepEqual( assert.deepEqual(
peer.record.get(who, 'profile'), peer.record.get(aliceID, 'profile'),
{ name: 'Alice', age: 20 }, { name: 'Alice', age: 20 },
'get' 'get'
) )
const fieldRoots3 = peer.record.getFieldRoots(who, 'profile') const fieldRoots3 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals( assert.deepEqual(
fieldRoots3, fieldRoots3,
{ name: ['WGDGt1UEGPpRyutfDyC2we'], age: ['XqkG9Uz1eQcxv9R1f3jgKS'] }, { age: ['9iTTqNabtnXmw4AiZxNMRq'], name: ['M2JhM7TE2KX5T5rfnxBh6M'] },
'fieldRoots' 'fieldRoots'
) )
}) })
test('Record squeeze', async (t) => { test('Record squeeze', async (t) => {
t.ok(await p(peer.record.update)(who, 'profile', { age: 21 }), 'update .age') assert(await p(peer.record.update)(aliceID, 'profile', { age: 21 }), 'update .age')
t.ok(await p(peer.record.update)(who, 'profile', { age: 22 }), 'update .age') assert(await p(peer.record.update)(aliceID, 'profile', { age: 22 }), 'update .age')
t.ok(await p(peer.record.update)(who, 'profile', { age: 23 }), 'update .age') assert(await p(peer.record.update)(aliceID, 'profile', { age: 23 }), 'update .age')
const fieldRoots4 = peer.record.getFieldRoots(who, 'profile') const fieldRoots4 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals( assert.deepEqual(
fieldRoots4, fieldRoots4,
{ name: ['WGDGt1UEGPpRyutfDyC2we'], age: ['6qu5mbLbFPJHCFge7QtU48'] }, { name: ['M2JhM7TE2KX5T5rfnxBh6M'], age: ['S3xiydrT6Y34Bp1vg6wN7P'] },
'fieldRoots' 'fieldRoots'
) )
t.equals(peer.record._squeezePotential('profile'), 3, 'squeezePotential=3') assert.equal(peer.record._squeezePotential('profile'), 3, 'squeezePotential=3')
t.true(await p(peer.record.squeeze)(who, 'profile'), 'squeezed') assert.equal(await p(peer.record.squeeze)(aliceID, 'profile'), true, 'squeezed')
const fieldRoots5 = peer.record.getFieldRoots(who, 'profile') const fieldRoots5 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals( assert.deepEqual(
fieldRoots5, fieldRoots5,
{ name: ['Ba96TjutuuPbdMMvNS4BbL'], age: ['Ba96TjutuuPbdMMvNS4BbL'] }, { name: ['Y4JkpPCHN8Avtz4VALaAmK'], age: ['Y4JkpPCHN8Avtz4VALaAmK'] },
'fieldRoots' 'fieldRoots'
) )
t.equals(peer.record._squeezePotential('profile'), 0, 'squeezePotential=0') assert.equal(peer.record._squeezePotential('profile'), 0, 'squeezePotential=0')
t.false(await p(peer.record.squeeze)(who, 'profile'), 'squeeze idempotent') assert.equal(await p(peer.record.squeeze)(aliceID, 'profile'), false,'squeeze idempotent')
const fieldRoots6 = peer.record.getFieldRoots(who, 'profile') const fieldRoots6 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals(fieldRoots6, fieldRoots5, 'fieldRoots') assert.deepEqual(fieldRoots6, fieldRoots5, 'fieldRoots')
}) })
test('Record receives old branched update', async (t) => { test('Record receives old branched update', async (t) => {
const rootMsg = FeedV1.createRoot(aliceKeys, 'record_v1__profile') const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair)
const rootHash = FeedV1.getMsgHash(rootMsg) const mootID = MsgV3.getMsgID(moot)
const tangle = new FeedV1.Tangle(rootHash) const tangle = new MsgV3.Tangle(mootID)
tangle.add(rootHash, rootMsg) tangle.add(mootID, moot)
await p(peer.db.add)(moot, mootID)
const msg = FeedV1.create({ const msg = MsgV3.create({
keys: aliceKeys, keypair: aliceKeypair,
type: 'record_v1__profile', domain: 'record_v1__profile',
content: { update: { age: 2 }, supersedes: [] }, account: aliceID,
accountTips: [aliceID],
data: { update: { age: 2 }, supersedes: [] },
tangles: { tangles: {
[rootHash]: tangle, [mootID]: tangle,
}, },
}) })
const rec = await p(peer.db.add)(msg, rootHash) const rec = await p(peer.db.add)(msg, mootID)
t.equals(rec.hash, 'JXvFSXE9s1DF77cSu5XUm', 'msg hash') assert.equal(rec.id, 'XZWr3DZFG253awsWXgSkS2', 'msg ID')
const fieldRoots7 = peer.record.getFieldRoots(who, 'profile') const fieldRoots7 = peer.record.getFieldRoots(aliceID, 'profile')
t.deepEquals( assert.deepEqual(
fieldRoots7, fieldRoots7,
{ {
name: ['Ba96TjutuuPbdMMvNS4BbL'], name: ['Y4JkpPCHN8Avtz4VALaAmK'],
age: ['Ba96TjutuuPbdMMvNS4BbL', rec.hash], age: ['Y4JkpPCHN8Avtz4VALaAmK', rec.id],
}, },
'fieldRoots' 'fieldRoots'
) )
t.equals(peer.record._squeezePotential('profile'), 6, 'squeezePotential=6') assert.equal(peer.record._squeezePotential('profile'), 6, 'squeezePotential=6')
}) })
test('teardown', (t) => { test('teardown', async (t) => {
peer.close(true, t.end) await p(peer.close)(true)
}) })

View File

@ -1,14 +1,37 @@
const ssbKeys = require('ssb-keys') const os = require('node:os')
const SSBURI = require('ssb-uri2') const path = require('node:path')
const base58 = require('bs58') const rimraf = require('rimraf')
const caps = require('ppppp-caps')
const Keypair = require('ppppp-keypair')
function generateKeypair(seed) { function createPeer(opts) {
const keys = ssbKeys.generate('ed25519', seed, 'buttwoo-v1') if (opts.name) {
const { data } = SSBURI.decompose(keys.id) opts.path ??= path.join(os.tmpdir(), 'tanglesync-' + opts.name)
keys.id = `ppppp:feed/v1/${base58.encode(Buffer.from(data, 'base64'))}` opts.keypair ??= Keypair.generate('ed25519', opts.name)
return keys 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('ssb-box'))
.use(require('../lib'))
.call(null, {
caps,
connections: {
incoming: {
net: [{ scope: 'device', transform: 'shse', port: null }],
},
outgoing: {
net: [{ transform: 'shse' }],
},
},
...opts,
})
} }
module.exports = { module.exports = { createPeer }
generateKeypair,
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"include": ["lib/**/*.js"],
"exclude": ["coverage/", "node_modules/", "test/"],
"compilerOptions": {
"checkJs": true,
"declaration": true,
"emitDeclarationOnly": true,
"exactOptionalPropertyTypes": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2022", "dom"],
"module": "node16",
"skipLibCheck": true,
"strict": true,
"target": "es2021"
}
}