OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-orgs commits / af86ba2

HyperOrgs: extraSchemas — compose issue ops onto the org room (#42)
Constructor accepts { extraSchemas } and builds every org Room with composeSchemas([orgsSchema, ...extraSchemas]); both construction sites (create + _openOrgRoom) use it. The daemon injects [issuesSchema] so an org Room IS its own issue tracker — org issues live in the org autobase and replicate WITH it. A peer that mounts only the org record autobase (peer mode) therefore gets the org's issues for free; no separate issues:org:<name> autobase, no issueBootstrapKey conveyance (#42). hyper-orgs stays issues-agnostic (never imports hyper-issues); the schema is injected. hyper-issues added as a test-only devDependency for the proof. Existing org behaviour unchanged when extraSchemas is empty (schema === orgsSchema). Proof: test/org-issues-linked-core.test.js — owner creates org+issue, peer mountOrg + replicate, peer sees the issue; control without issuesSchema does not. hyper-orgs regression 7/7 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
af86ba2c20d213d38b24f8d40cbe8c760343d5c8
by plankatron <fleeky@gmail.com> · 2026-05-27T01:46:15.000Z

Changed files (3) +83 -4

lib/hyper-orgs.js +11 -3
……@@ -24,7 +24,7 @@
2424 * today joiners are read-only followers of the org's autobase.
2525 */
2626
27import { Room, encodeInvite, decodeInvite, isExpired } from 'hyper-rooms'
27import { Room, composeSchemas, encodeInvite, decodeInvite, isExpired } from 'hyper-rooms'
2828import { orgsSchema, validateName } from './schema.js'
2929
3030export class HyperOrgs {
……@@ -35,6 +35,14 @@ export class HyperOrgs {
3535 */
3636 constructor (corestore, options = {}) {
3737 this._corestore = corestore.namespace(options.namespace || 'orgs')
38 // Extra schemas composed onto every org Room (issues-agnostic — the
39 // daemon injects issuesSchema so an org Room IS its own issue tracker;
40 // org issues then replicate WITH the org autobase). Stays decoupled:
41 // hyper-orgs never imports hyper-issues.
42 this._extraSchemas = options.extraSchemas || []
43 this._schema = this._extraSchemas.length
44 ? composeSchemas([orgsSchema, ...this._extraSchemas])
45 : orgsSchema
3846 // name -> { room: Room, role: string, created: boolean }
3947 this._orgs = new Map()
4048 // Local append-only index (NOT replicated): which orgs this instance
……@@ -68,7 +76,7 @@ export class HyperOrgs {
6876 // local writer identity is stable across restarts.
6977 async _openOrgRoom (name, bootstrapKey, { created, role }) {
7078 const namespace = created ? 'org/' + name : 'org-join/' + bootstrapKey.slice(0, 16)
71 const room = new Room(this._corestore, { schema: orgsSchema, namespace, bootstrapKey })
79 const room = new Room(this._corestore, { schema: this._schema, namespace, bootstrapKey })
7280 await room.ready()
7381 this._orgs.set(name, { room, role, created })
7482 return room
……@@ -150,7 +158,7 @@ export class HyperOrgs {
150158 if (this._orgs.has(name)) throw new Error(`Organization "${name}" already exists`)
151159
152160 const room = new Room(this._corestore, {
153 schema: orgsSchema,
161 schema: this._schema,
154162 namespace: 'org/' + name,
155163 bootstrapKey: null
156164 })
package.json +2 -1
……@@ -13,6 +13,7 @@
1313 "devDependencies": {
1414 "autobase-test-helpers": "^2.0.0",
1515 "brittle": "^3.0.0",
16 "corestore": "^7.0.0"
16 "corestore": "^7.0.0",
17 "hyper-issues": "file:../hyper-issues"
1718 }
1819}
test/org-issues-linked-core.test.js +70 -0
……@@ -0,0 +1,70 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import { mkdtempSync } from 'fs'
4import { join } from 'path'
5import { tmpdir } from 'os'
6import { replicateAndSync } from 'autobase-test-helpers'
7import { HyperOrgs } from '../index.js'
8import { HyperIssues, issuesSchema } from 'hyper-issues'
9
10function tmp () { return mkdtempSync(join(tmpdir(), 'orgs-issues-')) }
11
12// #42 fix, proven at the library layer.
13//
14// An org Room built with composeSchemas([orgsSchema, issuesSchema])
15// carries its issues IN the org's own autobase. So a peer that joins
16// ONLY the org's record autobase (peer mode — mountOrg) also gets its
17// issues, with NO separate tracker autobase and NO issueBootstrapKey
18// conveyance. This is the linked-core (Keet db+blobs) shape.
19test('org issues live in the org room and replicate WITH the org', async (t) => {
20 const owner = new HyperOrgs(new Corestore(tmp()), {
21 namespace: 'owner', extraSchemas: [issuesSchema]
22 })
23 await owner.ready()
24 await owner.create('alpha', 'group', { displayName: 'Alpha' })
25
26 const ownerRoom = owner.getOrgRoom('alpha')
27 const ownerIssues = new HyperIssues(null, { room: ownerRoom, ownsRoom: false })
28 await ownerIssues.ready()
29 const id = await ownerIssues.create({ title: 'org issue', body: 'b', author: 'owner' })
30
31 // Peer mounts ONLY alpha's record autobase (peer mode).
32 const peer = new HyperOrgs(new Corestore(tmp()), {
33 namespace: 'peer', extraSchemas: [issuesSchema]
34 })
35 await peer.ready()
36 await peer.mountOrg('alpha', ownerRoom.keyHex)
37 await replicateAndSync([owner.getOrgBase('alpha'), peer.getOrgBase('alpha')])
38
39 // Org record replicated (sanity).
40 const org = await peer.get('alpha')
41 t.is(org && org.displayName, 'Alpha', 'org record replicated to peer')
42
43 // The org issue replicated WITH the org — no separate tracker.
44 const peerIssues = new HyperIssues(null, { room: peer.getOrgRoom('alpha'), ownsRoom: false })
45 await peerIssues.ready()
46 const got = await peerIssues.get(id)
47 t.is(got && got.title, 'org issue', 'org issue replicated to peer via the org autobase')
48
49 await ownerIssues.close()
50 await peerIssues.close()
51 await owner.close()
52 await peer.close()
53})
54
55// Control: WITHOUT extraSchemas the org room ignores issue ops, so the
56// issue never materialises (proves the replication above is due to the
57// composed schema, not some other path).
58test('control: org room without issuesSchema does not store issues', async (t) => {
59 const owner = new HyperOrgs(new Corestore(tmp()), { namespace: 'owner' }) // no extraSchemas
60 await owner.ready()
61 await owner.create('beta', 'group', { displayName: 'Beta' })
62 const room = owner.getOrgRoom('beta')
63 const issues = new HyperIssues(null, { room, ownsRoom: false })
64 await issues.ready()
65 const id = await issues.create({ title: 'ignored', author: 'owner' })
66 const got = await issues.get(id)
67 t.absent(got, 'issue op ignored when org room has no issuesSchema composed')
68 await issues.close()
69 await owner.close()
70})