| … | … | @@ -213,6 +213,43 @@ export class HyperOrgs { |
| 213 | 213 | return this.get(name) |
| 214 | 214 | } |
| 215 | 215 | |
| 216 | // ── Record meta (generic set-meta op) ──────────────────────── | |
| 217 | // Thin typed accessors over the schema's `set-meta` op (meta:{key} → | |
| 218 | // {value}). The daemon uses these to publish per-daemon origin coordinates | |
| 219 | // (serverKey + repoEventLog) into the org autobase, so every org peer can | |
| 220 | // boot a reverse RepoPuller toward the others — the org-scoped analogue of | |
| 221 | // the IdentityRegistry mirror-writer announce (#16, __server__/__repoeventlog__). | |
| 222 | // Only a WRITER on the org can setMeta; readers replicate + read it. | |
| 223 | ||
| 224 | async setMeta (orgName, key, value) { | |
| 225 | const room = this._requireRoom(orgName) | |
| 226 | await room.append({ type: 'set-meta', key, value, timestamp: Date.now() }) | |
| 227 | await room.base.update() | |
| 228 | return value | |
| 229 | } | |
| 230 | ||
| 231 | async getMeta (orgName, key) { | |
| 232 | const room = this._requireRoom(orgName) | |
| 233 | await room.base.update() | |
| 234 | const e = await room.view.get(`meta:${key}`) | |
| 235 | return e ? e.value.value : null | |
| 236 | } | |
| 237 | ||
| 238 | // List meta entries whose key starts with `prefix` (e.g. 'peer:'), returning | |
| 239 | // [{ key, value }] with the `meta:` view-prefix stripped from key. Suffixed | |
| 240 | // keys are strictly > the bare prefix, so `gt` (matching the grants scan | |
| 241 | // convention) never misses an entry. | |
| 242 | async listMeta (orgName, prefix = '') { | |
| 243 | const room = this._requireRoom(orgName) | |
| 244 | await room.base.update() | |
| 245 | const out = [] | |
| 246 | const lo = `meta:${prefix}` | |
| 247 | for await (const entry of room.view.createReadStream({ gt: lo, lt: lo + '\xff' })) { | |
| 248 | out.push({ key: entry.key.slice('meta:'.length), value: entry.value.value }) | |
| 249 | } | |
| 250 | return out | |
| 251 | } | |
| 252 | ||
| 216 | 253 | async delete (name) { |
| 217 | 254 | const org = await this.get(name) |
| 218 | 255 | if (!org) throw new Error(`Organization "${name}" not found`) |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "hyper-orgs", |
| 3 | "version": "0.2.1", | |
| 3 | "version": "0.2.2", | |
| 4 | 4 | "description": "P2P organization management — a hyper-rooms schema for orgs, members, resources, grants, invites, and access requests", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -0,0 +1,47 @@ |
| 1 | import test from 'brittle' | |
| 2 | import Corestore from 'corestore' | |
| 3 | import { mkdtempSync } from 'fs' | |
| 4 | import { join } from 'path' | |
| 5 | import { tmpdir } from 'os' | |
| 6 | import { HyperOrgs } from '../index.js' | |
| 7 | ||
| 8 | function tmpStore () { | |
| 9 | const dir = mkdtempSync(join(tmpdir(), 'hyper-orgs-meta-')) | |
| 10 | return new Corestore(dir) | |
| 11 | } | |
| 12 | ||
| 13 | async function makeOrgs (t, namespace = 'meta') { | |
| 14 | const store = tmpStore() | |
| 15 | const orgs = new HyperOrgs(store, { namespace }) | |
| 16 | await orgs.ready() | |
| 17 | t.teardown(async () => { | |
| 18 | try { await orgs.close() } catch {} | |
| 19 | try { await store.close() } catch {} | |
| 20 | }) | |
| 21 | return orgs | |
| 22 | } | |
| 23 | ||
| 24 | // setMeta/getMeta/listMeta back the org-scoped peer-coordinate announce: a | |
| 25 | // writer publishes meta:peer:<serverKey> -> { serverKey, repoEventLog } into | |
| 26 | // the org autobase so every org peer can boot a reverse RepoPuller toward it. | |
| 27 | test('setMeta / getMeta / listMeta — peer coordinates round-trip', async (t) => { | |
| 28 | const orgs = await makeOrgs(t, 'meta-1') | |
| 29 | await orgs.create('house', 'group') | |
| 30 | ||
| 31 | const coordsA = { serverKey: 'a'.repeat(64), repoEventLog: 'c'.repeat(64) } | |
| 32 | const coordsB = { serverKey: 'b'.repeat(64), repoEventLog: 'd'.repeat(64) } | |
| 33 | await orgs.setMeta('house', 'peer:' + coordsA.serverKey, coordsA) | |
| 34 | await orgs.setMeta('house', 'peer:' + coordsB.serverKey, coordsB) | |
| 35 | // a non-peer meta entry must NOT show up under the 'peer:' prefix scan | |
| 36 | await orgs.setMeta('house', 'schema-version', 2) | |
| 37 | ||
| 38 | t.alike(await orgs.getMeta('house', 'peer:' + coordsA.serverKey), coordsA, 'getMeta returns the stored value') | |
| 39 | t.is(await orgs.getMeta('house', 'nope'), null, 'missing meta → null') | |
| 40 | ||
| 41 | const peers = await orgs.listMeta('house', 'peer:') | |
| 42 | t.is(peers.length, 2, 'listMeta returns only the peer: entries (schema-version excluded)') | |
| 43 | const byServer = new Map(peers.map(p => [p.value.serverKey, p.value])) | |
| 44 | t.alike(byServer.get('a'.repeat(64)), coordsA, 'peer A coords intact') | |
| 45 | t.alike(byServer.get('b'.repeat(64)), coordsB, 'peer B coords intact') | |
| 46 | t.ok(peers.every(p => p.key.startsWith('peer:')), 'meta: view-prefix stripped, peer: retained') | |
| 47 | }) |