rename this package from record=>dict

This commit is contained in:
Andre Staltz 2023-10-26 13:15:28 +03:00
parent edaffa7626
commit 46dafa436e
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
6 changed files with 111 additions and 106 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@ node_modules
pnpm-lock.yaml
package-lock.json
coverage
**/*.d.ts
*~
# For misc scripts and experiments:

View File

@ -5,5 +5,5 @@
We're not on npm yet. In your package.json, include this as
```js
"ppppp-record": "github:staltz/ppppp-record"
"ppppp-dict": "github:staltz/ppppp-dict"
```

View File

@ -1,6 +1,6 @@
const MsgV3 = require('ppppp-db/msg-v3')
const PREFIX = 'record_v1__'
const PREFIX = 'dict_v1__'
/**
* @typedef {ReturnType<import('ppppp-db').init>} PPPPPDB
@ -18,11 +18,13 @@ const PREFIX = 'record_v1__'
* @typedef {string} MsgID
* @typedef {`${Subdomain}.${string}`} SubdomainField
* @typedef {{
* update: Record<string, any>,
* update: {
* [field in string]: any
* },
* supersedes: Array<MsgID>,
* }} RecordData
* }} DictMsgData
* @typedef {{
* record?: {
* dict?: {
* ghostSpan?: number
* }
* }} Config
@ -65,18 +67,18 @@ function fromSubdomain(subdomain) {
* @returns {asserts peer is { db: PPPPPDB, close: ClosableHook }}
*/
function assertDBPlugin(peer) {
if (!peer.db) throw new Error('record plugin requires ppppp-db plugin')
if (!peer.db) throw new Error('dict plugin requires ppppp-db plugin')
}
/**
* @param {{ db: PPPPPDB | null, close: ClosableHook }} peer
* @param {Config} config
*/
function initRecord(peer, config) {
function initDict(peer, config) {
assertDBPlugin(peer)
const ghostSpan = config.record?.ghostSpan ?? 32
if (ghostSpan < 1) throw new Error('config.record.ghostSpan must be >= 0')
const ghostSpan = config.dict?.ghostSpan ?? 32
if (ghostSpan < 1) throw new Error('config.dict.ghostSpan must be >= 0')
//#region state
let accountID = /** @type {string | null} */ (null)
@ -96,10 +98,10 @@ function initRecord(peer, config) {
},
/**
* @param {string} subdomain
* @returns {Record<string, Array<MsgID>>}
* @returns {{[field in string]: Array<MsgID>}}
*/
getAll(subdomain) {
const out = /** @type {Record<string, Array<MsgID>>} */ ({})
const out = /** @type {{[field in string]: Array<MsgID>}} */ ({})
for (const [key, value] of this._map.entries()) {
if (key.startsWith(subdomain + '.')) {
const field = key.slice(subdomain.length + 1)
@ -160,7 +162,7 @@ function initRecord(peer, config) {
* @param {Msg | null | undefined} msg
* @returns {msg is Msg}
*/
function isValidRecordMoot(msg) {
function isValidDictMoot(msg) {
if (!msg) return false
if (msg.metadata.account !== accountID) return false
const domain = msg.metadata.domain
@ -171,9 +173,9 @@ function initRecord(peer, config) {
/**
* @private
* @param {Msg | null | undefined} msg
* @returns {msg is Msg<RecordData>}
* @returns {msg is Msg<DictMsgData>}
*/
function isValidRecordMsg(msg) {
function isValidDictMsg(msg) {
if (!msg) return false
if (!msg.data) return false
if (msg.metadata.account !== accountID) return false
@ -190,7 +192,7 @@ function initRecord(peer, config) {
* @param {string} mootID
* @param {Msg} moot
*/
function learnRecordMoot(mootID, moot) {
function learnDictMoot(mootID, moot) {
const subdomain = toSubdomain(moot.metadata.domain)
const tangle = tangles.get(subdomain) ?? new MsgV3.Tangle(mootID)
tangle.add(mootID, moot)
@ -200,9 +202,9 @@ function initRecord(peer, config) {
/**
* @private
* @param {string} msgID
* @param {Msg<RecordData>} msg
* @param {Msg<DictMsgData>} msg
*/
function learnRecordUpdate(msgID, msg) {
function learnDictUpdate(msgID, msg) {
const { account, domain } = msg.metadata
const mootID = MsgV3.getMootID(account, domain)
const subdomain = toSubdomain(domain)
@ -232,14 +234,14 @@ function initRecord(peer, config) {
* @param {string} msgID
* @param {Msg} msg
*/
function maybeLearnAboutRecord(msgID, msg) {
function maybeLearnAboutDict(msgID, msg) {
if (msg.metadata.account !== accountID) return
if (isValidRecordMoot(msg)) {
learnRecordMoot(msgID, msg)
if (isValidDictMoot(msg)) {
learnDictMoot(msgID, msg)
return
}
if (isValidRecordMsg(msg)) {
learnRecordUpdate(msgID, msg)
if (isValidDictMsg(msg)) {
learnDictUpdate(msgID, msg)
return
}
}
@ -279,7 +281,7 @@ function initRecord(peer, config) {
/**
* @param {string} subdomain
* @param {Record<string, any>} update
* @param {{[field in string]: any}} update
* @param {CB<boolean>} cb
*/
function forceUpdate(subdomain, update, cb) {
@ -298,7 +300,7 @@ function initRecord(peer, config) {
{ account: accountID, domain, data: { update, supersedes } },
(err, rec) => {
// 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 Dict', { cause: err }))
// @ts-ignore
cb(null, true)
}
@ -317,12 +319,12 @@ function initRecord(peer, config) {
loadPromise = new Promise((resolve, reject) => {
for (const rec of peer.db.records()) {
if (!rec.msg) continue
maybeLearnAboutRecord(rec.id, rec.msg)
maybeLearnAboutDict(rec.id, rec.msg)
}
cancelOnRecordAdded = peer.db.onRecordAdded(
(/** @type {RecPresent} */ rec) => {
try {
maybeLearnAboutRecord(rec.id, rec.msg)
maybeLearnAboutDict(rec.id, rec.msg)
} catch (err) {
console.error(err)
}
@ -355,7 +357,7 @@ function initRecord(peer, config) {
const domain = rootMsg.metadata.domain
// prettier-ignore
if (!domain.startsWith(PREFIX)) throw new Error(`"${tangleID}" is not a record moot`)
if (!domain.startsWith(PREFIX)) throw new Error(`"${tangleID}" is not a dict moot`)
// Discover field roots
const fieldRoots = new Set()
@ -393,7 +395,7 @@ function initRecord(peer, config) {
* @public
* @param {string} id
* @param {string} subdomain
* @returns {Record<string, any> | null}
* @returns {{[field in string]: any} | null}
*/
function read(id, subdomain) {
assertDBPlugin(peer)
@ -405,15 +407,15 @@ function initRecord(peer, config) {
else return null
}
const msgIDs = tangle.topoSort()
const record = /** @type {Record<string, any>}*/ ({})
const dict = /** @type {{[field in string]: any}} */ ({})
for (const msgID of msgIDs) {
const msg = peer.db.get(msgID)
if (isValidRecordMsg(msg)) {
if (isValidDictMsg(msg)) {
const { update } = msg.data
Object.assign(record, update)
Object.assign(dict, update)
}
}
return record
return dict
}
/**
@ -445,7 +447,7 @@ function initRecord(peer, config) {
// prettier-ignore
if (!msg) throw new Error(`isGhostable() msgID "${ghostableMsgID}" does not exist in the database`)
// prettier-ignore
if (!isValidRecordMoot(tangle.root)) throw new Error(`isGhostable() tangleID "${tangleID}" is not a record`)
if (!isValidDictMoot(tangle.root)) throw new Error(`isGhostable() tangleID "${tangleID}" is not a dict`)
// Discover field roots
const fieldRootIDs = new Set()
@ -486,7 +488,7 @@ function initRecord(peer, config) {
/**
* @public
* @param {string} subdomain
* @param {Record<string, any>} update
* @param {{[field in string]: any}} update
* @param {CB<boolean>} cb
*/
function update(subdomain, update, cb) {
@ -494,13 +496,13 @@ function initRecord(peer, config) {
loaded(() => {
if (!accountID) return cb(new Error('Expected account to be loaded'))
const record = read(accountID, subdomain)
const dict = read(accountID, subdomain)
// prettier-ignore
if (!record) return cb(new Error(`Cannot update non-existent record "${subdomain}`))
if (!dict) return cb(new Error(`Cannot update non-existent dict "${subdomain}`))
let hasChanges = false
for (const [field, value] of Object.entries(update)) {
if (value !== record[field]) {
if (value !== dict[field]) {
hasChanges = true
break
}
@ -521,12 +523,12 @@ function initRecord(peer, config) {
loaded(() => {
if (!accountID) return cb(new Error('Expected account to be loaded'))
const record = read(accountID, subdomain)
const dict = read(accountID, subdomain)
// prettier-ignore
if (!record) return cb(new Error(`Cannot squeeze non-existent record "${subdomain}`))
forceUpdate(subdomain, record, (err, _forceUpdated) => {
if (!dict) return cb(new Error(`Cannot squeeze non-existent Dict "${subdomain}"`))
forceUpdate(subdomain, dict, (err, _forceUpdated) => {
// 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 Dict "${subdomain}"`, { cause: err }))
// @ts-ignore
cb(null, true)
})
@ -550,5 +552,5 @@ function initRecord(peer, config) {
}
}
exports.name = 'record'
exports.init = initRecord
exports.name = 'dict'
exports.init = initDict

View File

@ -1,13 +1,13 @@
{
"name": "ppppp-record",
"name": "ppppp-dict",
"version": "1.0.0",
"description": "Record data structure over append-only logs with pruning",
"description": "Dictionary data structure over append-only logs with pruning",
"author": "Andre Staltz <contact@staltz.com>",
"license": "MIT",
"homepage": "https://github.com/staltz/ppppp-record",
"homepage": "https://github.com/staltz/ppppp-dict",
"repository": {
"type": "git",
"url": "git@github.com:staltz/ppppp-record.git"
"url": "git@github.com:staltz/ppppp-dict.git"
},
"main": "index.js",
"files": [

View File

@ -18,12 +18,12 @@ E-->D & C
classDef default fill:#bbb,stroke:#fff0,color:#000
```
Reducing the tangle above in a topological sort allows you to build a record
Reducing the tangle above in a topological sort allows you to build a dict
(a JSON object) `{age, name}`.
## Msg metadata domain
`msg.metadata.domain` MUST start with `record_v1__`. E.g. `record_v1__profile`.
`msg.metadata.domain` MUST start with `dict_v1__`. E.g. `dict_v1__profile`.
## Msg data
@ -31,7 +31,9 @@ Reducing the tangle above in a topological sort allows you to build a record
```typescript
interface MsgData {
update: Record<string, any>,
update: {
[field in string]: any,
},
supersedes: Array<MsgHash>,
}
```
@ -40,11 +42,11 @@ RECOMMENDED that the `msg.data.update` is as flat as possible (no nesting).
## Supersedes links
When you update a field in a record, in the `supersedes` array you MUST point
When you update a field in a dict, in the `supersedes` array you MUST point
to the currently-known highest-depth msg that updated that field.
The set of *not-transitively-superseded-by-anyone* msgs comprise the
"field roots" of the record. To allow pruning the tangle, we can delete
"field roots" of the dict. To allow pruning the tangle, we can delete
(or, if we want to keep metadata, "erase") all msgs preceding the field roots.
Suppose the tangle is grown in the order below, then the field roots are

View File

@ -8,7 +8,7 @@ const Keypair = require('ppppp-keypair')
const p = require('util').promisify
const { createPeer } = require('./util')
const DIR = path.join(os.tmpdir(), 'ppppp-record')
const DIR = path.join(os.tmpdir(), 'ppppp-dict')
rimraf.sync(DIR)
const aliceKeypair = Keypair.generate('ed25519', 'alice')
@ -19,7 +19,7 @@ test('setup', async (t) => {
peer = createPeer({
keypair: aliceKeypair,
path: DIR,
record: { ghostSpan: 4 },
dict: { ghostSpan: 4 },
})
await peer.db.loaded()
@ -28,143 +28,143 @@ test('setup', async (t) => {
domain: 'account',
_nonce: 'alice',
})
await p(peer.record.load)(aliceID)
await p(peer.dict.load)(aliceID)
assert.equal(peer.record.getGhostSpan(), 4, 'getGhostSpan')
assert.equal(peer.dict.getGhostSpan(), 4, 'getGhostSpan')
})
test('Record update() and get()', async (t) => {
test('Dict update() and get()', async (t) => {
assert(
await p(peer.record.update)('profile', { name: 'alice' }),
await p(peer.dict.update)('profile', { name: 'alice' }),
'update .name'
)
assert.deepEqual(
peer.record.read(aliceID, 'profile'),
peer.dict.read(aliceID, 'profile'),
{ name: 'alice' },
'get'
)
const fieldRoots1 = peer.record._getFieldRoots('profile')
const fieldRoots1 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots1,
{ name: ['PbwnLbJS4oninQ1RPCdgRn'] },
{ name: ['QZSb3GMTRWWUUVLtueNB7Q'] },
'fieldRoots'
)
assert(await p(peer.record.update)('profile', { age: 20 }), 'update .age')
assert(await p(peer.dict.update)('profile', { age: 20 }), 'update .age')
assert.deepEqual(
peer.record.read(aliceID, 'profile'),
peer.dict.read(aliceID, 'profile'),
{ name: 'alice', age: 20 },
'get'
)
const fieldRoots2 = peer.record._getFieldRoots('profile')
const fieldRoots2 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots2,
{ name: ['PbwnLbJS4oninQ1RPCdgRn'], age: ['9iTTqNabtnXmw4AiZxNMRq'] },
{ name: ['QZSb3GMTRWWUUVLtueNB7Q'], age: ['98QTF8Zip6NYJgmcf96L2K'] },
'fieldRoots'
)
assert.equal(
await p(peer.record.update)('profile', { name: 'alice' }),
await p(peer.dict.update)('profile', { name: 'alice' }),
false,
'redundant update .name'
)
assert.deepEqual(
peer.record.read(aliceID, 'profile'),
peer.dict.read(aliceID, 'profile'),
{ name: 'alice', age: 20 },
'get'
)
assert.equal(
await p(peer.record.update)('profile', { name: 'Alice' }),
await p(peer.dict.update)('profile', { name: 'Alice' }),
true,
'update .name'
)
assert.deepEqual(
peer.record.read(aliceID, 'profile'),
peer.dict.read(aliceID, 'profile'),
{ name: 'Alice', age: 20 },
'get'
)
const fieldRoots3 = peer.record._getFieldRoots('profile')
const fieldRoots3 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots3,
{ age: ['9iTTqNabtnXmw4AiZxNMRq'], name: ['M2JhM7TE2KX5T5rfnxBh6M'] },
{ age: ['98QTF8Zip6NYJgmcf96L2K'], name: ['49rg6mJFDgdq6kZTE8uedr'] },
'fieldRoots'
)
})
test('Record squeeze', async (t) => {
assert(await p(peer.record.update)('profile', { age: 21 }), 'update .age')
assert(await p(peer.record.update)('profile', { age: 22 }), 'update .age')
assert(await p(peer.record.update)('profile', { age: 23 }), 'update .age')
test('Dict squeeze', async (t) => {
assert(await p(peer.dict.update)('profile', { age: 21 }), 'update .age')
assert(await p(peer.dict.update)('profile', { age: 22 }), 'update .age')
assert(await p(peer.dict.update)('profile', { age: 23 }), 'update .age')
const fieldRoots4 = peer.record._getFieldRoots('profile')
const fieldRoots4 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots4,
{ name: ['M2JhM7TE2KX5T5rfnxBh6M'], age: ['S3xiydrT6Y34Bp1vg6wN7P'] },
{ name: ['49rg6mJFDgdq6kZTE8uedr'], age: ['GE9KcJc5efunBhSTDjy6zX'] },
'fieldRoots'
)
assert.equal(
peer.record._squeezePotential('profile'),
peer.dict._squeezePotential('profile'),
3,
'squeezePotential=3'
)
assert.equal(await p(peer.record.squeeze)('profile'), true, 'squeezed')
assert.equal(await p(peer.dict.squeeze)('profile'), true, 'squeezed')
const fieldRoots5 = peer.record._getFieldRoots('profile')
const fieldRoots5 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots5,
{ name: ['Y4JkpPCHN8Avtz4VALaAmK'], age: ['Y4JkpPCHN8Avtz4VALaAmK'] },
{ name: ['Xr7DZdwaANzPByUdRYGb2E'], age: ['Xr7DZdwaANzPByUdRYGb2E'] },
'fieldRoots'
)
assert.equal(
peer.record._squeezePotential('profile'),
peer.dict._squeezePotential('profile'),
0,
'squeezePotential=0'
)
assert.equal(
await p(peer.record.squeeze)('profile'),
await p(peer.dict.squeeze)('profile'),
false,
'squeeze idempotent'
)
const fieldRoots6 = peer.record._getFieldRoots('profile')
const fieldRoots6 = peer.dict._getFieldRoots('profile')
assert.deepEqual(fieldRoots6, fieldRoots5, 'fieldRoots')
})
test('Record isGhostable', (t) => {
const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair)
test('Dict isGhostable', (t) => {
const moot = MsgV3.createMoot(aliceID, 'dict_v1__profile', aliceKeypair)
const mootID = MsgV3.getMsgID(moot)
assert.equal(mootID, peer.record.getFeedID('profile'), 'getFeedID')
assert.equal(mootID, peer.dict.getFeedID('profile'), 'getFeedID')
const tangle = peer.db.getTangle(mootID)
const msgIDs = tangle.topoSort()
const fieldRoots = peer.record._getFieldRoots('profile')
const fieldRoots = peer.dict._getFieldRoots('profile')
assert.deepEqual(fieldRoots.age, [msgIDs[7]])
// Remember from the setup, that ghostSpan=4
assert.equal(msgIDs.length, 8)
assert.equal(peer.record.isGhostable(msgIDs[0], mootID), false) // moot
assert.equal(peer.record.isGhostable(msgIDs[1], mootID), false)
assert.equal(peer.record.isGhostable(msgIDs[2], mootID), false)
assert.equal(peer.record.isGhostable(msgIDs[3], mootID), true) // in ghostSpan
assert.equal(peer.record.isGhostable(msgIDs[4], mootID), true) // in ghostSpan
assert.equal(peer.record.isGhostable(msgIDs[5], mootID), true) // in ghostSpan
assert.equal(peer.record.isGhostable(msgIDs[6], mootID), true) // in ghostSpan
assert.equal(peer.record.isGhostable(msgIDs[7], mootID), false) // field root
assert.equal(peer.dict.isGhostable(msgIDs[0], mootID), false) // moot
assert.equal(peer.dict.isGhostable(msgIDs[1], mootID), false)
assert.equal(peer.dict.isGhostable(msgIDs[2], mootID), false)
assert.equal(peer.dict.isGhostable(msgIDs[3], mootID), true) // in ghostSpan
assert.equal(peer.dict.isGhostable(msgIDs[4], mootID), true) // in ghostSpan
assert.equal(peer.dict.isGhostable(msgIDs[5], mootID), true) // in ghostSpan
assert.equal(peer.dict.isGhostable(msgIDs[6], mootID), true) // in ghostSpan
assert.equal(peer.dict.isGhostable(msgIDs[7], mootID), false) // field root
})
test('Record receives old branched update', async (t) => {
const moot = MsgV3.createMoot(aliceID, 'record_v1__profile', aliceKeypair)
test('Dict receives old branched update', async (t) => {
const moot = MsgV3.createMoot(aliceID, 'dict_v1__profile', aliceKeypair)
const mootID = MsgV3.getMsgID(moot)
assert.equal(peer.record.minRequiredDepth(mootID), 7, 'minRequiredDepth')
assert.equal(peer.dict.minRequiredDepth(mootID), 7, 'minRequiredDepth')
const tangle = new MsgV3.Tangle(mootID)
tangle.add(mootID, moot)
@ -172,7 +172,7 @@ test('Record receives old branched update', async (t) => {
const msg = MsgV3.create({
keypair: aliceKeypair,
domain: 'record_v1__profile',
domain: 'dict_v1__profile',
account: aliceID,
accountTips: [aliceID],
data: { update: { age: 2 }, supersedes: [] },
@ -181,22 +181,22 @@ test('Record receives old branched update', async (t) => {
},
})
const rec = await p(peer.db.add)(msg, mootID)
assert.equal(rec.id, 'XZWr3DZFG253awsWXgSkS2', 'msg ID')
assert.equal(rec.id, 'PBq5dgfK9icRVx7SLhyaC5', 'msg ID')
const fieldRoots7 = peer.record._getFieldRoots('profile')
const fieldRoots7 = peer.dict._getFieldRoots('profile')
assert.deepEqual(
fieldRoots7,
{
name: ['Y4JkpPCHN8Avtz4VALaAmK'],
age: ['Y4JkpPCHN8Avtz4VALaAmK', rec.id],
name: ['Xr7DZdwaANzPByUdRYGb2E'],
age: ['Xr7DZdwaANzPByUdRYGb2E', rec.id],
},
'fieldRoots'
)
assert.equal(peer.record.minRequiredDepth(mootID), 1, 'minRequiredDepth')
assert.equal(peer.dict.minRequiredDepth(mootID), 1, 'minRequiredDepth')
assert.equal(
peer.record._squeezePotential('profile'),
peer.dict._squeezePotential('profile'),
6,
'squeezePotential=6'
)