/**
* HyperAtomSpace - Basal AtomSpace with Hypercore persistence
*
* Adds to basal-atomspace:
* - Persistent storage via Hypercore (append-only log)
* - Indexes via Hyperbee (B-tree on Hypercore)
* - P2P replication via Hyperswarm
* - Offline-first, sync when connected
*/
import Hypercore from 'hypercore'
import Hyperbee from 'hyperbee'
import Hyperswarm from 'hyperswarm'
import b4a from 'b4a'
import c from 'compact-encoding'
// ============================================================================
// ENCODING - How atoms are serialized (~40 lines)
// ============================================================================
const atomEncoding = {
encode(atom) {
return c.encode(c.any, {
t: atom.type,
n: atom.name || null,
o: atom.outgoing?.map(a => a.hash) || null,
v: atom.values ? Object.fromEntries(atom.values) : null
})
},
decode(buf) {
return c.decode(c.any, buf)
}
}
// ============================================================================
// HYPER ATOMSPACE (~150 lines)
// ============================================================================
class HyperAtomSpace {
constructor(storage, key = null) {
this.storage = storage
this.key = key
// In-memory cache (subset of full atomspace)
this.atoms = new Map() // hash -> Atom
this.byType = new Map() // type -> Set<hash>
this.byName = new Map() // "type:name" -> hash
this.ready = this._init()
}
async _init() {
// Main data store (append-only log of atoms)
this.core = new Hypercore(this.storage, this.key, {
valueEncoding: atomEncoding
})
await this.core.ready()
// B-tree indexes
this.db = new Hyperbee(this.core, {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
await this.db.ready()
// Load existing atoms into memory
await this._loadAll()
return this
}
async _loadAll() {
for await (const { key, value } of this.db.createReadStream({
gte: 'atom:',
lt: 'atom:\xff'
})) {
const hash = key.slice(5) // Remove 'atom:' prefix
this._cacheAtom(hash, value)
}
}
_cacheAtom(hash, data) {
// Reconstruct atom from stored data
const atom = data.n !== null
? { type: data.t, name: data.n, hash, values: new Map(Object.entries(data.v || {})) }
: { type: data.t, outgoing: data.o, hash, values: new Map(Object.entries(data.v || {})) }
this.atoms.set(hash, atom)
// Index by type
if (!this.byType.has(data.t)) this.byType.set(data.t, new Set())
this.byType.get(data.t).add(hash)
// Index nodes by name
if (data.n !== null) {
this.byName.set(`${data.t}:${data.n}`, hash)
}
return atom
}
// Compute hash for an atom (before it exists)
_hash(type, name, outgoing) {
const crypto = await import('crypto')
if (name !== null) {
return crypto.createHash('sha256')
.update(`N:${type}:${name}`)
.digest('hex').slice(0, 16)
} else {
const outHashes = outgoing.join(',')
return crypto.createHash('sha256')
.update(`L:${type}:[${outHashes}]`)
.digest('hex').slice(0, 16)
}
}
/**
* Add a node
*/
async addNode(type, name) {
await this.ready
const key = `${type}:${name}`
// Check cache first
if (this.byName.has(key)) {
return this.atoms.get(this.byName.get(key))
}
// Check persistent storage
const existing = await this.db.get(`name:${key}`)
if (existing) {
return this._cacheAtom(existing.value.hash, existing.value)
}
// Create new
const crypto = await import('crypto')
const hash = crypto.createHash('sha256')
.update(`N:${type}:${name}`)
.digest('hex').slice(0, 16)
const data = { t: type, n: name, o: null, v: {} }
// Persist
const batch = this.db.batch()
await batch.put(`atom:${hash}`, data)
await batch.put(`name:${key}`, { hash, ...data })
await batch.put(`type:${type}:${hash}`, hash)
await batch.flush()
return this._cacheAtom(hash, data)
}
/**
* Add a link
*/
async addLink(type, outgoing) {
await this.ready
// Ensure all outgoing atoms exist and get their hashes
const outHashes = outgoing.map(a => a.hash)
const crypto = await import('crypto')
const hash = crypto.createHash('sha256')
.update(`L:${type}:[${outHashes.join(',')}]`)
.digest('hex').slice(0, 16)
// Check if exists
if (this.atoms.has(hash)) {
return this.atoms.get(hash)
}
const existing = await this.db.get(`atom:${hash}`)
if (existing) {
return this._cacheAtom(hash, existing.value)
}
// Create new
const data = { t: type, n: null, o: outHashes, v: {} }
// Persist
const batch = this.db.batch()
await batch.put(`atom:${hash}`, data)
await batch.put(`type:${type}:${hash}`, hash)
// Index by each outgoing atom (incoming set)
for (const outHash of outHashes) {
await batch.put(`incoming:${outHash}:${hash}`, hash)
}
await batch.flush()
const atom = this._cacheAtom(hash, data)
// Resolve outgoing references
atom.outgoing = outHashes.map(h => this.atoms.get(h))
return atom
}
/**
* Set a value on an atom
*/
async setValue(atom, key, value) {
await this.ready
atom.values.set(key, value)
// Update in storage
const existing = await this.db.get(`atom:${atom.hash}`)
if (existing) {
existing.value.v = existing.value.v || {}
existing.value.v[key] = value
await this.db.put(`atom:${atom.hash}`, existing.value)
}
}
/**
* Query by type
*/
async getByType(type) {
await this.ready
const results = []
for await (const { value } of this.db.createReadStream({
gte: `type:${type}:`,
lt: `type:${type}:\xff`
})) {
const atom = this.atoms.get(value)
if (atom) results.push(atom)
}
return results
}
/**
* Get incoming links
*/
async getIncoming(atom, linkType = null) {
await this.ready
const results = []
for await (const { value } of this.db.createReadStream({
gte: `incoming:${atom.hash}:`,
lt: `incoming:${atom.hash}:\xff`
})) {
const link = this.atoms.get(value)
if (link && (!linkType || link.type === linkType)) {
results.push(link)
}
}
return results
}
/**
* Simple pattern query
*/
async query(pattern) {
await this.ready
const candidates = await this.getByType(pattern.type)
return candidates.filter(atom => {
if (!atom.outgoing) return false
if (pattern.outgoing?.length !== atom.outgoing.length) return false
for (let i = 0; i < (pattern.outgoing?.length || 0); i++) {
const p = pattern.outgoing[i]
const a = atom.outgoing[i]
if (p === null) continue // wildcard
if (p.hash && p.hash !== a.hash) return false
if (p.type && p.type !== a.type) return false
if (p.name && a.name !== p.name) return false
}
return true
})
}
// ========== P2P SYNC ==========
/**
* Join swarm and replicate
*/
async joinSwarm(topic) {
await this.ready
this.swarm = new Hyperswarm()
const topicBuffer = b4a.isBuffer(topic)
? topic
: b4a.from(topic, 'hex')
this.swarm.join(topicBuffer)
this.swarm.on('connection', (conn) => {
console.log('Peer connected')
this.core.replicate(conn)
})
return this.swarm
}
/**
* Get public key for sharing
*/
get publicKey() {
return this.core.key?.toString('hex')
}
// ========== STATS ==========
get size() {
return this.atoms.size
}
}
// ============================================================================
// DEMO
// ============================================================================
async function demo() {
const as = new HyperAtomSpace('./test-atomspace')
await as.ready
console.log('HyperAtomSpace initialized')
console.log('Public key:', as.publicKey)
// Create knowledge
const dog = await as.addNode('ConceptNode', 'dog')
const cat = await as.addNode('ConceptNode', 'cat')
const animal = await as.addNode('ConceptNode', 'animal')
const dogIsAnimal = await as.addLink('InheritanceLink', [dog, animal])
const catIsAnimal = await as.addLink('InheritanceLink', [cat, animal])
// Set truth value
await as.setValue(dogIsAnimal, 'truth', [1.0, 0.99])
// Query
const animals = await as.query({
type: 'InheritanceLink',
outgoing: [null, animal]
})
console.log('\nThings that are animals:')
for (const link of animals) {
console.log(' ', link.outgoing[0].name)
}
console.log('\nAtomSpace size:', as.size)
}
export { HyperAtomSpace }
// Run demo if executed directly
const isMain = import.meta.url === `file://${process.argv[1]}`
if (isMain) {
demo().catch(console.error)
}