This commit is contained in:
Raven Scott
2026-05-20 22:56:32 -04:00
parent b8c335adee
commit dd384eb944
321 changed files with 7484 additions and 3793 deletions
@@ -1,28 +1,33 @@
# hyper-pear-runtime-session
**Status:** scaffold (`0.0.0-scaffold`) · **Protocol:** `pear-runtime-session/v1` · **Wave:** 8
Production module: Runtime sessions.
Pear runtime session bridge.
**Protocol:** `pear-runtime-session/v1`
## Holepunch references (inspiration only)
## When to use
- `pear-runtime`
Session lifecycle.
> This module composes on Hyperswarm/Hypercore — it does **not** re-implement upstream packages.
## When not to use
## Composes with
Cross-process sync.
- `hyper-p2p-presence`
## Quick start
## Planned API
```js
const { HyperPearRuntimeSession } = require('hyper-pear-runtime-session')
const m = new HyperPearRuntimeSession()
await m.ready()
await m.close()
```
- `constructor(opts)` — topic, optional keyPair
- `getStats()` — scaffold counters
- `ready()` — no-op until implemented
- Domain methods — throw `not implemented: scaffold` until Wave 8+ pass
## Docs
## Layout
- [docs/api.md](docs/api.md)
- [docs/architecture.md](docs/architecture.md)
`modules/pear-platform/hyper-pear-runtime-session/`
## Test
See [`modules/_shared/MODULE_SYSTEM.md`](../../_shared/MODULE_SYSTEM.md).
```bash
npm install && npm test
```
@@ -1,23 +1,26 @@
# hyper-pear-runtime-session API
# API: hyper-pear-runtime-session
**Status:** scaffold · **Protocol:** `pear-runtime-session/v1`
**Protocol:** `pear-runtime-session/v1` · **Export:** `HyperPearRuntimeSession`
## Class `HyperP2PPearRuntimeSession`
## Methods
Scaffold stub — methods throw `not implemented: scaffold` until promoted to production tier.
### `startSession(...)`
### `constructor(opts?)`
Domain API.
### `getStats()`
### `endSession(...)`
Returns `{ created, errors, protocol, tier: 'scaffold' }`.
Domain API.
### `ready()`
### `activeSessions(...)`
Resolves immediately (no-op).
Domain API.
## Wire (planned)
| Message | Direction | Notes |
|---------|-----------|-------|
| TBD | gossip | Defined in implementation pass |
### `getStats()` / `ready()` / `close()`
Lifecycle helpers.
## P2P
Local-only.
@@ -1,15 +1,11 @@
# hyper-pear-runtime-session architecture
# Architecture: hyper-pear-runtime-session
**Tier:** scaffold · **Category:** `pear-platform`
## Wire messages
## Role
| type | fields | direction | behavior |
|------|--------|-----------|----------|
| — | — | — | Local-only |
Pear runtime session bridge.
## State
## Composition
Uses `../../_shared/p2p-bare.js` for Hyperswarm + Protomux when implemented. Does **not** duplicate Holepunch core storage/transport.
## Holepunch boundary
Inspiration: n/a
In-memory structures; gossip via `initModuleSwarm` / `gossipSend` when P2P enabled.
@@ -1,8 +1,8 @@
require('bare-process/global')
const { HyperP2PPearRuntimeSession } = require('../index.js')
const { HyperPearRuntimeSession } = require('../index.js')
async function main () {
const m = new HyperP2PPearRuntimeSession()
console.log('[scaffold]', m.getStats())
const m = new HyperPearRuntimeSession()
console.log(m.getStats())
await m.close()
}
main().catch(console.error)
@@ -1,60 +1,72 @@
require('bare-process/global')
const EventEmitter = require('bare-events')
const crypto = require('hypercore-crypto')
const b4a = require('b4a')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const { attachGossip, sendGossip } = require('../../_shared/storage-gossip-base.js')
const PROTOCOL = 'pear-runtime-session/v1'
class HyperP2PPearRuntimeSession extends EventEmitter {
class HyperPearRuntimeSession extends EventEmitter {
constructor (opts = {}) {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._store = new Map()
this._stats = { ops: 0, gossipIn: 0, gossipOut: 0 }
this.swarm = null
this._sessions = new Map()
this._stats = { started: 0, ended: 0 }
this._seq = 0
}
put (key, value) {
assertNonEmpty(key, 'key')
this._store.set(key, value)
this._stats.ops++
sendGossip(this, { type: 'pear-runtime-session-sync', key, value })
this.emit('update', { key, value })
return true
startSession (meta) {
const id = b4a.toString(crypto.hash(b4a.from(`${Date.now()}-${this._seq++}`)).slice(0, 8), 'hex')
const session = {
id,
meta: meta || {},
startedAt: Date.now(),
endedAt: null,
active: true
}
this._sessions.set(id, session)
this._stats.started++
this.emit('started', session)
return session
}
get (key) { return this._store.get(key) }
delete (key) {
const ok = this._store.delete(key)
if (ok) sendGossip(this, { type: 'pear-runtime-session-sync', key, value: null })
return ok
endSession (id) {
assertNonEmpty(id, 'id')
const session = this._sessions.get(id)
if (!session) throw new Error(`session not found: ${id}`)
if (!session.active) throw new Error(`session already ended: ${id}`)
session.active = false
session.endedAt = Date.now()
this._stats.ended++
this.emit('ended', session)
return session
}
entries () { return [...this._store.entries()] }
activeSessions () {
return [...this._sessions.values()].filter((s) => s.active)
}
_onGossip (d) {
if (!d || d.type !== 'pear-runtime-session-sync') return
this._stats.gossipIn++
if (d.key !== undefined) {
if (d.value === null) this._store.delete(d.key)
else this._store.set(d.key, d.value)
getSession (id) {
assertNonEmpty(id, 'id')
return this._sessions.get(id) || null
}
getStats () {
return {
...this._stats,
total: this._sessions.size,
active: this.activeSessions().length,
protocol: PROTOCOL,
mode: 'local'
}
}
getStats () { return { ...this._stats, size: this._store.size, protocol: PROTOCOL } }
async ready () {
if (this.swarm || !this.topic) return this
await attachGossip(this, { keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL, onmessage: (d) => this._onGossip(d) })
return this
}
async close () {
if (this.swarm) await this.swarm.destroy().catch(() => {})
this.swarm = null
this.emit('closed')
}
}
module.exports = { HyperP2PPearRuntimeSession, PROTOCOL }
module.exports = { HyperPearRuntimeSession, HyperP2PPearRuntimeSession: HyperPearRuntimeSession, PROTOCOL }
@@ -1,26 +1,39 @@
require('bare-process/global')
const test = require('brittle')
const { HyperP2PPearRuntimeSession, PROTOCOL } = require('../index.js')
const { HyperPearRuntimeSession, PROTOCOL } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PPearRuntimeSession)
t.ok(PROTOCOL)
t.ok(HyperPearRuntimeSession)
t.is(PROTOCOL, 'pear-runtime-session/v1')
})
test('basic operation', async (t) => {
const m = new HyperP2PPearRuntimeSession()
m.put('k', 1); t.is(m.get('k'), 1)
test('startSession activeSessions', async (t) => {
const m = new HyperPearRuntimeSession()
const s = m.startSession({ app: 'demo' })
t.is(m.activeSessions().length, 1)
t.is(m.activeSessions()[0].id, s.id)
await m.close()
})
test('validation', async (t) => {
const m = new HyperP2PPearRuntimeSession()
try { m.put(null, 1) } catch (e) { t.ok(e) }
test('endSession removes from active', async (t) => {
const m = new HyperPearRuntimeSession()
const s = m.startSession({})
m.endSession(s.id)
t.is(m.activeSessions().length, 0)
await m.close()
})
test('endSession twice throws', async (t) => {
const m = new HyperPearRuntimeSession()
const s = m.startSession({})
m.endSession(s.id)
try { m.endSession(s.id) } catch (e) { t.ok(e) }
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PPearRuntimeSession()
t.ok(m.getStats().protocol)
const m = new HyperPearRuntimeSession()
m.startSession({})
t.is(m.getStats().started, 1)
await m.close()
})