Updates
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
const test = require('brittle')
|
||||
const { HyperP2PReactiveState } = require('../index.js')
|
||||
const b4a = require('b4a')
|
||||
const path = require('bare-path')
|
||||
const fs = require('bare-fs/promises')
|
||||
const process = require('bare-process')
|
||||
|
||||
test('hyper-p2p-reactive-state basic lifecycle and LWW CRDT', async (t) => {
|
||||
const cwd = process.cwd()
|
||||
const storageDir = path.join(cwd, 'test-reactive-storage-' + Date.now())
|
||||
const topic = 'test-reactive-topic-' + Date.now()
|
||||
|
||||
const state = new HyperP2PReactiveState({
|
||||
topic,
|
||||
storageDir,
|
||||
syncInterval: 2000,
|
||||
expiry: 10000,
|
||||
metadata: { test: true }
|
||||
})
|
||||
|
||||
let readyFired = false
|
||||
state.on('ready', () => { readyFired = true })
|
||||
|
||||
await state.ready()
|
||||
t.ok(readyFired, 'ready event fired')
|
||||
|
||||
// Test set
|
||||
const res1 = await state.set('user:alice', { name: 'Alice', score: 100 })
|
||||
t.ok(res1.timestamp, 'set returns timestamp')
|
||||
t.is(state.get('user:alice').value.name, 'Alice')
|
||||
|
||||
// Test update (should win by timestamp)
|
||||
await state.set('user:alice', { name: 'Alice Updated', score: 150 })
|
||||
t.is(state.get('user:alice').value.score, 150)
|
||||
|
||||
// Test query
|
||||
const results = state.query({ keyPrefix: 'user:' })
|
||||
t.ok(results.length >= 1, 'query returns results')
|
||||
|
||||
// Test delete
|
||||
await state.delete('user:alice')
|
||||
t.ok(state.get('user:alice').deleted, 'delete marks as deleted')
|
||||
|
||||
// Test subscribe
|
||||
let changeFired = false
|
||||
const unsub = state.subscribe('config:theme', (change) => {
|
||||
changeFired = true
|
||||
t.is(change.key, 'config:theme')
|
||||
t.is(change.value, 'dark')
|
||||
})
|
||||
await state.set('config:theme', 'dark')
|
||||
t.ok(changeFired, 'subscribe callback fired')
|
||||
|
||||
unsub()
|
||||
|
||||
// Test toJSON
|
||||
const json = state.toJSON()
|
||||
t.ok(json['config:theme'] === 'dark' || json['user:alice'] === null, 'toJSON works')
|
||||
|
||||
await state.close()
|
||||
t.pass('closed cleanly')
|
||||
|
||||
// Cleanup storage
|
||||
try {
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
test('hyper-p2p-reactive-state persistence across instances', async (t) => {
|
||||
const cwd = process.cwd()
|
||||
const storageDir = path.join(cwd, 'test-reactive-persist-' + Date.now())
|
||||
const topic = 'test-persist-topic-' + Date.now()
|
||||
|
||||
// First instance writes
|
||||
const state1 = new HyperP2PReactiveState({
|
||||
topic,
|
||||
storageDir,
|
||||
metadata: { instance: 1 }
|
||||
})
|
||||
await state1.ready()
|
||||
await state1.set('shared:key', 'hello-persist')
|
||||
await state1.close()
|
||||
|
||||
// Second instance loads from same storage
|
||||
const state2 = new HyperP2PReactiveState({
|
||||
topic,
|
||||
storageDir,
|
||||
metadata: { instance: 2 }
|
||||
})
|
||||
await state2.ready()
|
||||
const loaded = state2.get('shared:key')
|
||||
t.ok(loaded, 'persisted value loaded')
|
||||
t.is(loaded.value, 'hello-persist')
|
||||
|
||||
await state2.close()
|
||||
|
||||
try {
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
test('hyper-p2p-reactive-state LWW conflict resolution', async (t) => {
|
||||
const cwd = process.cwd()
|
||||
const storageDir = path.join(cwd, 'test-lww-' + Date.now())
|
||||
const topic = 'test-lww-topic'
|
||||
|
||||
const state = new HyperP2PReactiveState({ topic, storageDir })
|
||||
await state.ready()
|
||||
|
||||
const now = Date.now()
|
||||
// Simulate concurrent sets with different timestamps
|
||||
await state.set('conflict:key', 'first', { timestamp: now })
|
||||
await state.set('conflict:key', 'second', { timestamp: now + 100 })
|
||||
|
||||
t.is(state.get('conflict:key').value, 'second', 'later timestamp wins')
|
||||
|
||||
// Same timestamp, peerId tiebreak
|
||||
const peerA = 'aaa111'
|
||||
const peerB = 'bbb222'
|
||||
await state.set('tie:key', 'fromA', { timestamp: now, peerId: peerA })
|
||||
await state.set('tie:key', 'fromB', { timestamp: now, peerId: peerB })
|
||||
|
||||
t.is(state.get('tie:key').value, 'fromB', 'higher peerId wins tie')
|
||||
|
||||
await state.close()
|
||||
try {
|
||||
await fs.rm(storageDir, { recursive: true, force: true })
|
||||
} catch (e) {}
|
||||
})
|
||||
test('hyper-p2p-reactive-state: close without leak', async (t) => {
|
||||
const m = new HyperP2PReactiveState()
|
||||
await m.close()
|
||||
t.pass()
|
||||
})
|
||||
test('hyper-p2p-reactive-state: validation rejects invalid input', async (t) => {
|
||||
const m = new HyperP2PReactiveState()
|
||||
try {
|
||||
if (typeof m.addNeighbor === 'function') m.addNeighbor(null)
|
||||
else if (typeof m.buildCircuit === 'function') m.buildCircuit([])
|
||||
else if (typeof m.grant === 'function') m.grant(null, -1)
|
||||
else if (typeof m.enqueue === 'function') m.enqueue('bad', null)
|
||||
else if (typeof m.reportSample === 'function') m.reportSample(null, -1, -1)
|
||||
else if (typeof m.fanout === 'function') m.fanout(null, 0)
|
||||
else if (typeof m.probe === 'function') m.probe(null)
|
||||
else if (typeof m.resolve === 'function') m.resolve(null)
|
||||
else if (typeof m.acquire === 'function') m.acquire(null)
|
||||
else throw new Error('no validation hook')
|
||||
t.fail('expected throw')
|
||||
} catch (err) {
|
||||
t.ok(err instanceof Error)
|
||||
}
|
||||
await m.close()
|
||||
})
|
||||
Reference in New Issue
Block a user