OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-issues commits / 3c75fb4

Initial commit — hyper-issues
P2P issue tracking as a hyper-rooms schema. Ops: create, comment, label, close, reopen, add-ref, remove-ref. Apply function keeps per-issue denormalized views (counts, latest comment, labels) in Hyperbee indexes under the room's corestore namespace. Refactored from 898 LOC → 495 LOC when lifted onto hyper-rooms — the Room substrate absorbed writer management, replication, and schema versioning. Known limitation (documented): auto-increment issue IDs collide under concurrent writers; will switch to UUIDs later. 15 tests / all passing. Phase 4 of the pear-git-v3 bottom-up rebuild. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3c75fb42920bd32ec95eb83d930a63d5646e8879
by plankatron <fleeky@gmail.com> · 2026-04-15T03:40:44.000Z

Changed files (9) +2846 -0

.gitignore +15 -0
……@@ -0,0 +1,15 @@
1# Dependencies
2node_modules/
3
4# Storage directories created by example CLIs
5.hyper-issues-*/
6
7# Editor and OS cruft
8.DS_Store
9*.log
10*.swp
11.vscode/
12.idea/
13
14# Test artifacts
15coverage/
examples/cli.js +229 -0
……@@ -0,0 +1,229 @@
1#!/usr/bin/env node
2/**
3 * Mini CLI example for hyper-issues
4 *
5 * Usage:
6 * node examples/cli.js init Create a tracker
7 * node examples/cli.js info Show key + writers + counts
8 * node examples/cli.js create <title> [-- body...] Create an issue
9 * node examples/cli.js list [open|closed|all] List issues
10 * node examples/cli.js show <id> Show issue + comments
11 * node examples/cli.js close <id> Close an issue
12 * node examples/cli.js reopen <id> Reopen an issue
13 * node examples/cli.js comment <id> <body...> Add a comment
14 * node examples/cli.js label <id> <name1,name2,...> Add labels
15 * node examples/cli.js init-labels Create default labels
16 * node examples/cli.js demo Run a full demo
17 *
18 * Storage: HYPER_ISSUES_STORE env var (default: ./.hyper-issues-cli)
19 *
20 * NOTE: this CLI uses its own storage path. The web example uses a different
21 * default. They don't share state — RocksDB single-writer lock.
22 */
23
24import Corestore from 'corestore'
25import { HyperIssues } from '../index.js'
26
27const storagePath = process.env.HYPER_ISSUES_STORE || './.hyper-issues-cli'
28const author = process.env.USER || 'me'
29const args = process.argv.slice(2)
30const cmd = args[0]
31
32async function open () {
33 const store = new Corestore(storagePath)
34 await store.ready()
35 const tracker = new HyperIssues(store, { namespace: 'issues' })
36 await tracker.ready()
37 return { tracker, store }
38}
39
40async function close ({ tracker, store }) {
41 await tracker.close()
42 await store.close()
43}
44
45async function main () {
46 if (cmd === 'init') {
47 const ctx = await open()
48 console.log('Tracker initialized at: %s', storagePath)
49 console.log(' key: %s', ctx.tracker.keyHex)
50 console.log(' localKey: %s', ctx.tracker.localKeyHex)
51 console.log(' writable: %s', ctx.tracker.writable)
52 await close(ctx)
53 return
54 }
55
56 if (cmd === 'info') {
57 const ctx = await open()
58 const counts = await ctx.tracker.counts()
59 const writers = await ctx.tracker.listWriters()
60 const labels = await ctx.tracker.listLabels()
61 console.log('Tracker: %s', storagePath)
62 console.log(' key: %s', ctx.tracker.keyHex)
63 console.log(' localKey: %s', ctx.tracker.localKeyHex)
64 console.log(' writable: %s', ctx.tracker.writable)
65 console.log(' issues: %d open, %d closed', counts.open, counts.closed)
66 console.log(' writers: %d', writers.length)
67 console.log(' labels: %d', labels.length)
68 await close(ctx)
69 return
70 }
71
72 if (cmd === 'create') {
73 const dashIdx = args.indexOf('--')
74 const title = dashIdx > 1 ? args.slice(1, dashIdx).join(' ') : args.slice(1).join(' ')
75 const body = dashIdx > 1 ? args.slice(dashIdx + 1).join(' ') : ''
76 if (!title) { console.error('Usage: cli.js create <title> [-- body...]'); process.exit(1) }
77 const ctx = await open()
78 const id = await ctx.tracker.create({ title, body, author })
79 console.log('Created issue #%d: %s', id, title)
80 await close(ctx)
81 return
82 }
83
84 if (cmd === 'list') {
85 const status = args[1] || 'all'
86 const ctx = await open()
87 const issues = await ctx.tracker.list({ status, sort: 'newest' })
88 if (issues.length === 0) {
89 console.log('(no %s issues)', status)
90 } else {
91 for (const i of issues) {
92 const marker = i.status === 'closed' ? '[x]' : '[ ]'
93 const labels = (i.labels || []).map(l => '#' + l).join(' ')
94 console.log('%s #%-3d %s %s', marker, i.id, i.title, labels)
95 }
96 console.log('\n%d issues', issues.length)
97 }
98 await close(ctx)
99 return
100 }
101
102 if (cmd === 'show') {
103 const id = parseInt(args[1])
104 if (!id) { console.error('Usage: cli.js show <id>'); process.exit(1) }
105 const ctx = await open()
106 const issue = await ctx.tracker.get(id)
107 if (!issue) {
108 console.error('Issue #%d not found', id)
109 await close(ctx)
110 process.exit(1)
111 }
112 console.log('#%d %s [%s]', issue.id, issue.title, issue.status)
113 console.log('by %s · %s', issue.author, new Date(issue.createdAt).toISOString())
114 if ((issue.labels || []).length > 0) console.log('labels: %s', issue.labels.map(l => '#' + l).join(' '))
115 if (issue.body) console.log('\n%s', issue.body)
116
117 const comments = await ctx.tracker.getComments(id)
118 if (comments.length > 0) {
119 console.log('\n--- %d comments ---', comments.length)
120 for (const c of comments) {
121 console.log(' by %s · %s', c.author, new Date(c.createdAt).toISOString())
122 console.log(' %s\n', c.body)
123 }
124 }
125
126 await close(ctx)
127 return
128 }
129
130 if (cmd === 'close' || cmd === 'reopen') {
131 const id = parseInt(args[1])
132 if (!id) { console.error('Usage: cli.js ' + cmd + ' <id>'); process.exit(1) }
133 const ctx = await open()
134 const status = cmd === 'close' ? 'closed' : 'open'
135 await ctx.tracker.update(id, { status }, author)
136 console.log('Issue #%d → %s', id, status)
137 await close(ctx)
138 return
139 }
140
141 if (cmd === 'comment') {
142 const id = parseInt(args[1])
143 const body = args.slice(2).join(' ')
144 if (!id || !body) { console.error('Usage: cli.js comment <id> <body...>'); process.exit(1) }
145 const ctx = await open()
146 const cId = await ctx.tracker.comment(id, { body, author })
147 console.log('Comment %s added to #%d', cId.slice(0, 12), id)
148 await close(ctx)
149 return
150 }
151
152 if (cmd === 'label') {
153 const id = parseInt(args[1])
154 const labels = (args[2] || '').split(',').map(l => l.trim()).filter(Boolean)
155 if (!id || labels.length === 0) { console.error('Usage: cli.js label <id> <name1,name2,...>'); process.exit(1) }
156 const ctx = await open()
157 await ctx.tracker.addLabels(id, labels, author)
158 console.log('Added labels to #%d: %s', id, labels.join(', '))
159 await close(ctx)
160 return
161 }
162
163 if (cmd === 'init-labels') {
164 const ctx = await open()
165 const labels = await ctx.tracker.initializeDefaultLabels()
166 console.log('Initialized %d default labels:', labels.length)
167 for (const l of labels) console.log(' #%s', l.name)
168 await close(ctx)
169 return
170 }
171
172 if (cmd === 'demo') {
173 const ctx = await open()
174 console.log('--- hyper-issues demo ---\n')
175
176 console.log('1. Initialize default labels')
177 await ctx.tracker.initializeDefaultLabels()
178
179 console.log('2. Create three issues')
180 const id1 = await ctx.tracker.create({ title: 'Login button broken', body: 'Clicking does nothing', author: 'alice' })
181 const id2 = await ctx.tracker.create({ title: 'Add dark mode', body: 'It would be nice', author: 'bob' })
182 const id3 = await ctx.tracker.create({ title: 'Update README', author: 'alice' })
183
184 console.log('3. Add labels')
185 await ctx.tracker.addLabels(id1, ['bug'], 'alice')
186 await ctx.tracker.addLabels(id2, ['enhancement'], 'bob')
187 await ctx.tracker.addLabels(id3, ['documentation'], 'alice')
188
189 console.log('4. Comment on issue #1')
190 await ctx.tracker.comment(id1, { body: 'I can reproduce on Firefox 130', author: 'bob' })
191 await ctx.tracker.comment(id1, { body: 'Found the bug — fix incoming', author: 'carol' })
192
193 console.log('5. Close issue #3')
194 await ctx.tracker.update(id3, { status: 'closed' }, 'alice')
195
196 console.log('\n6. List all issues:')
197 const all = await ctx.tracker.list({ sort: 'oldest' })
198 for (const i of all) {
199 const marker = i.status === 'closed' ? '[x]' : '[ ]'
200 const labels = (i.labels || []).map(l => '#' + l).join(' ')
201 console.log(' %s #%d %s %s', marker, i.id, i.title, labels)
202 }
203
204 const counts = await ctx.tracker.counts()
205 console.log('\n%d open · %d closed · %d total', counts.open, counts.closed, counts.total)
206
207 await close(ctx)
208 return
209 }
210
211 console.log('hyper-issues example CLI')
212 console.log(' init Create a tracker')
213 console.log(' info Show key + counts + writers')
214 console.log(' create <title> [-- body...] Create an issue')
215 console.log(' list [open|closed|all] List issues')
216 console.log(' show <id> Show issue + comments')
217 console.log(' close <id> | reopen <id> Change status')
218 console.log(' comment <id> <body...> Add a comment')
219 console.log(' label <id> <name1,name2,...> Add labels')
220 console.log(' init-labels Create default labels')
221 console.log(' demo Run a full demo')
222 console.log('')
223 console.log('Storage: HYPER_ISSUES_STORE env var (default: ./.hyper-issues-cli)')
224 console.log('Author: USER env var (default: me)')
225 console.log('Note: this CLI does NOT share state with examples/web.js — they use')
226 console.log('different storage paths because RocksDB only allows one writer.')
227}
228
229main().catch((err) => { console.error(err.message); process.exit(1) })
examples/web.js +254 -0
……@@ -0,0 +1,254 @@
1#!/usr/bin/env node
2/**
3 * Mini web example for hyper-issues
4 *
5 * SSR issue tracker dashboard. Pages:
6 * GET / → list issues + create form
7 * GET /:id → show issue + comments + comment form
8 * POST /create → create an issue
9 * POST /:id/close → close an issue
10 * POST /:id/reopen → reopen an issue
11 * POST /:id/comment → add a comment
12 *
13 * Usage: node examples/web.js [--port 9096] [--storage <path>]
14 */
15
16import http from 'http'
17import Corestore from 'corestore'
18import b4a from 'b4a'
19import { HyperIssues } from '../index.js'
20
21const args = process.argv.slice(2)
22function getFlag (name) {
23 const i = args.indexOf('--' + name)
24 return i !== -1 && args[i + 1] ? args[i + 1] : null
25}
26
27const port = parseInt(getFlag('port')) || 9096
28const storagePath = getFlag('storage') || './.hyper-issues-web'
29
30const store = new Corestore(storagePath)
31await store.ready()
32const tracker = new HyperIssues(store, { namespace: 'issues' })
33await tracker.ready()
34await tracker.initializeDefaultLabels()
35
36function escapeHtml (s) {
37 return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
38}
39
40function collectBody (req) {
41 return new Promise((resolve, reject) => {
42 const parts = []
43 req.on('data', chunk => parts.push(chunk))
44 req.on('end', () => resolve(b4a.concat(parts).toString('utf8')))
45 req.on('error', reject)
46 })
47}
48
49const httpServer = http.createServer(async (req, res) => {
50 const url = req.url.split('?')[0]
51 try {
52 if (req.method === 'GET' && url === '/') {
53 const filter = (req.url.split('?')[1] || '').match(/status=([^&]+)/)
54 const status = filter ? filter[1] : 'open'
55 const issues = await tracker.list({ status, sort: 'newest' })
56 const counts = await tracker.counts()
57 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
58 res.end(renderIndex(issues, counts, status))
59 return
60 }
61
62 const showMatch = url.match(/^\/(\d+)$/)
63 if (req.method === 'GET' && showMatch) {
64 const id = parseInt(showMatch[1])
65 const issue = await tracker.get(id)
66 if (!issue) {
67 res.writeHead(404, { 'Content-Type': 'text/plain' })
68 res.end('Not found')
69 return
70 }
71 const comments = await tracker.getComments(id)
72 res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
73 res.end(renderIssue(issue, comments))
74 return
75 }
76
77 if (req.method === 'POST' && url === '/create') {
78 const params = new URLSearchParams(await collectBody(req))
79 const title = params.get('title')
80 const body = params.get('body') || ''
81 const author = params.get('author') || 'anon'
82 if (title) {
83 const id = await tracker.create({ title, body, author })
84 res.writeHead(303, { Location: '/' + id })
85 res.end()
86 return
87 }
88 res.writeHead(400)
89 res.end('title required')
90 return
91 }
92
93 const closeMatch = url.match(/^\/(\d+)\/(close|reopen)$/)
94 if (req.method === 'POST' && closeMatch) {
95 const id = parseInt(closeMatch[1])
96 const status = closeMatch[2] === 'close' ? 'closed' : 'open'
97 await tracker.update(id, { status }, 'web')
98 res.writeHead(303, { Location: '/' + id })
99 res.end()
100 return
101 }
102
103 const commentMatch = url.match(/^\/(\d+)\/comment$/)
104 if (req.method === 'POST' && commentMatch) {
105 const id = parseInt(commentMatch[1])
106 const params = new URLSearchParams(await collectBody(req))
107 const body = params.get('body')
108 const author = params.get('author') || 'anon'
109 if (body) await tracker.comment(id, { body, author })
110 res.writeHead(303, { Location: '/' + id })
111 res.end()
112 return
113 }
114
115 res.writeHead(404, { 'Content-Type': 'text/plain' })
116 res.end('Not found')
117 } catch (err) {
118 console.error(err)
119 res.writeHead(500, { 'Content-Type': 'text/plain' })
120 res.end(err.message)
121 }
122})
123
124function styles () {
125 return `
126 body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 800px; margin: 0 auto; }
127 h1 { color: #58a6ff; }
128 h2 { font-size: 1rem; color: #c9d1d9; margin-top: 0; }
129 a { color: #58a6ff; text-decoration: none; }
130 a:hover { text-decoration: underline; }
131 .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
132 .card h2 { margin: 0 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: #8b949e; }
133 .row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; }
134 .row:last-child { border-bottom: none; }
135 .label { color: #8b949e; }
136 .filter { display: inline-block; padding: 0.3rem 0.6rem; margin-right: 0.3rem; border-radius: 4px; background: #21262d; color: #c9d1d9; }
137 .filter.active { background: #1f6feb; color: white; }
138 .issue { padding: 0.6rem 0.5rem; border-bottom: 1px solid #21262d; }
139 .issue:last-child { border-bottom: none; }
140 .issue .id { color: #8b949e; font-size: 0.8rem; }
141 .issue .title { color: #c9d1d9; font-weight: bold; }
142 .issue.closed .title { color: #8b949e; text-decoration: line-through; }
143 .issue .meta { color: #6e7681; font-size: 0.75rem; margin-top: 0.2rem; }
144 .tag { display: inline-block; padding: 0.05rem 0.4rem; margin-right: 0.3rem; border-radius: 999px; font-size: 0.7rem; background: #1f6feb22; border: 1px solid #1f6feb44; color: #58a6ff; }
145 .badge-open { background: #2ea04322; color: #3fb950; border: 1px solid #2ea04344; padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
146 .badge-closed { background: #8957e522; color: #a371f7; border: 1px solid #8957e544; padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.75rem; }
147 input, textarea, button { background: #0d1117; color: #c9d1d9; border: 1px solid #30363d; padding: 0.4rem 0.6rem; border-radius: 4px; font-family: monospace; font-size: 0.85rem; }
148 textarea { width: 100%; min-height: 4em; }
149 input { width: 100%; box-sizing: border-box; }
150 button { background: #21262d; cursor: pointer; }
151 button:hover { background: #30363d; }
152 button.btn-primary { background: #1f6feb; border-color: #1f6feb; color: white; }
153 button.btn-primary:hover { background: #1158c7; }
154 .field { margin-bottom: 0.5rem; }
155 .field label { display: block; color: #8b949e; font-size: 0.75rem; margin-bottom: 0.2rem; }
156 .comment { padding: 0.6rem 0; border-bottom: 1px solid #21262d; }
157 .comment:last-child { border-bottom: none; }
158 .comment .meta { color: #8b949e; font-size: 0.75rem; }
159 .empty { color: #484f58; font-style: italic; padding: 1rem 0; }
160 form { margin: 0; }
161 `
162}
163
164function renderIndex (issues, counts, currentStatus) {
165 return `<!DOCTYPE html>
166<html><head><title>hyper-issues</title><style>${styles()}</style></head><body>
167<h1><a href="/">hyper-issues</a></h1>
168
169<div class="card">
170 <h2>Filter</h2>
171 <a class="filter ${currentStatus === 'open' ? 'active' : ''}" href="/?status=open">${counts.open} open</a>
172 <a class="filter ${currentStatus === 'closed' ? 'active' : ''}" href="/?status=closed">${counts.closed} closed</a>
173 <a class="filter ${currentStatus === 'all' ? 'active' : ''}" href="/?status=all">${counts.total} all</a>
174</div>
175
176<div class="card">
177 <h2>${issues.length} ${currentStatus} issue${issues.length === 1 ? '' : 's'}</h2>
178 ${issues.length === 0 ? '<div class="empty">(no issues)</div>' : issues.map(i => `
179 <div class="issue ${i.status}">
180 <div>
181 <span class="id">#${i.id}</span>
182 <a class="title" href="/${i.id}">${escapeHtml(i.title)}</a>
183 ${(i.labels || []).map(l => '<span class="tag">' + escapeHtml(l) + '</span>').join(' ')}
184 </div>
185 <div class="meta">by ${escapeHtml(i.author)} · ${new Date(i.createdAt).toLocaleString()}</div>
186 </div>`).join('')}
187</div>
188
189<div class="card">
190 <h2>Create issue</h2>
191 <form action="/create" method="POST">
192 <div class="field"><label>Title</label><input name="title" required></div>
193 <div class="field"><label>Body</label><textarea name="body"></textarea></div>
194 <div class="field"><label>Author</label><input name="author" value="anon"></div>
195 <button class="btn-primary" type="submit">Create</button>
196 </form>
197</div>
198
199</body></html>`
200}
201
202function renderIssue (issue, comments) {
203 const badge = issue.status === 'closed'
204 ? '<span class="badge-closed">closed</span>'
205 : '<span class="badge-open">open</span>'
206 return `<!DOCTYPE html>
207<html><head><title>hyper-issues #${issue.id}</title><style>${styles()}</style></head><body>
208<h1><a href="/">hyper-issues</a> / #${issue.id}</h1>
209
210<div class="card">
211 <h2>Issue</h2>
212 <div style="display: flex; justify-content: space-between; align-items: center">
213 <div>
214 <strong>${escapeHtml(issue.title)}</strong>
215 ${badge}
216 </div>
217 <form action="/${issue.id}/${issue.status === 'open' ? 'close' : 'reopen'}" method="POST">
218 <button type="submit">${issue.status === 'open' ? 'Close' : 'Reopen'}</button>
219 </form>
220 </div>
221 <div class="meta" style="color:#8b949e;font-size:0.75rem;margin-top:0.5rem">
222 by ${escapeHtml(issue.author)} · ${new Date(issue.createdAt).toLocaleString()}
223 </div>
224 ${(issue.labels || []).length > 0 ? '<div style="margin-top:0.5rem">' + issue.labels.map(l => '<span class="tag">' + escapeHtml(l) + '</span>').join(' ') + '</div>' : ''}
225 ${issue.body ? '<div style="margin-top:0.8rem;white-space:pre-wrap;color:#c9d1d9">' + escapeHtml(issue.body) + '</div>' : ''}
226</div>
227
228<div class="card">
229 <h2>${comments.length} Comment${comments.length === 1 ? '' : 's'}</h2>
230 ${comments.length === 0 ? '<div class="empty">(no comments yet)</div>' : comments.map(c => `
231 <div class="comment">
232 <div class="meta">${escapeHtml(c.author)} · ${new Date(c.createdAt).toLocaleString()}${c.editedAt ? ' (edited)' : ''}</div>
233 <div style="white-space:pre-wrap;margin-top:0.3rem">${escapeHtml(c.body)}</div>
234 </div>`).join('')}
235</div>
236
237<div class="card">
238 <h2>Add comment</h2>
239 <form action="/${issue.id}/comment" method="POST">
240 <div class="field"><label>Body</label><textarea name="body" required></textarea></div>
241 <div class="field"><label>Author</label><input name="author" value="anon"></div>
242 <button class="btn-primary" type="submit">Comment</button>
243 </form>
244</div>
245
246</body></html>`
247}
248
249httpServer.listen(port, () => {
250 console.log('hyper-issues dashboard:')
251 console.log(' http://localhost:%d', port)
252 console.log(' storage: %s', storagePath)
253 console.log(' tracker key: %s', tracker.keyHex)
254})
index.js +6 -0
……@@ -0,0 +1,6 @@
1/**
2 * hyper-issues — P2P issue tracking on hyper-rooms
3 */
4
5export { HyperIssues } from './lib/hyper-issues.js'
6export { issuesSchema, DEFAULT_LABELS } from './lib/schema.js'
lib/hyper-issues.js +391 -0
……@@ -0,0 +1,391 @@
1/**
2 * HyperIssues — P2P issue tracking built on hyper-rooms
3 *
4 * Multi-writer, replicating, offline-first issue tracker. Wraps a Room with
5 * the issuesSchema and provides a high-level API for issues, comments,
6 * labels, and (optionally) git refs.
7 *
8 * Usage:
9 *
10 * const tracker = new HyperIssues(corestore, { namespace: 'my-project' })
11 * await tracker.ready()
12 *
13 * const id = await tracker.create({ title: 'Bug', body: 'Details', author: 'alice' })
14 * const issue = await tracker.get(id)
15 * await tracker.comment(id, { body: 'reproduced', author: 'bob' })
16 * const issues = await tracker.list({ status: 'open' })
17 *
18 * tracker.localKey // exchange via P2P for addWriter on the inviter
19 */
20
21import { Room } from 'hyper-rooms'
22import { issuesSchema, DEFAULT_LABELS } from './schema.js'
23
24export class HyperIssues {
25 /**
26 * @param {Corestore} corestore
27 * @param {object} options
28 * @param {string} [options.namespace='issues']
29 * @param {Buffer|string|null} [options.bootstrapKey=null]
30 */
31 constructor (corestore, options = {}) {
32 this._room = new Room(corestore, {
33 schema: issuesSchema,
34 namespace: options.namespace || 'issues',
35 bootstrapKey: options.bootstrapKey || null
36 })
37 }
38
39 // ── Lifecycle / properties ──────────────────────────────────
40
41 async ready () {
42 await this._room.ready()
43 }
44
45 async sync () { await this._room.base.update() }
46 async close () { await this._room.close() }
47
48 get key () { return this._room.key }
49 get keyHex () { return this._room.keyHex }
50 get discoveryKey () { return this._room.discoveryKey }
51 get localKey () { return this._room.localKey }
52 get localKeyHex () { return this._room.localKeyHex }
53 get writable () { return this._room.writable }
54 get view () { return this._room.view }
55 get base () { return this._room.base }
56 get room () { return this._room }
57
58 // ── Writer management (delegated to Room) ───────────────────
59
60 async addWriter (userId, options = {}) {
61 return this._room.addWriter(userId, options)
62 }
63
64 async removeWriter (userId) {
65 return this._room.removeWriter(userId)
66 }
67
68 async listWriters () {
69 return this._room.listWriters()
70 }
71
72 async getWriter (userId) {
73 return this._room.getWriter(userId)
74 }
75
76 // ── Issues ──────────────────────────────────────────────────
77
78 /**
79 * Create an issue with auto-incrementing ID.
80 * @param {object} options
81 * @param {string} options.title
82 * @param {string} [options.body]
83 * @param {string} options.author
84 * @param {string[]} [options.labels]
85 * @param {string[]} [options.images]
86 * @returns {Promise<number>} the issue ID
87 */
88 async create (options) {
89 const { title, body, author, labels, images } = options
90 await this._room.base.update()
91 const counter = await this._room.view.get('counter:issues')
92 const id = counter ? counter.value.next : 1
93
94 await this._room.append({
95 type: 'create-issue',
96 id,
97 title,
98 body: body || '',
99 userId: author,
100 labels: labels || [],
101 images: images || [],
102 timestamp: Date.now()
103 })
104 return id
105 }
106
107 /**
108 * Update an issue (title, body, status, labels, images).
109 * @param {number} id
110 * @param {object} updates
111 * @param {string} userId
112 */
113 async update (id, updates, userId) {
114 await this._room.append({
115 type: 'update-issue',
116 id,
117 ...updates,
118 userId,
119 timestamp: Date.now()
120 })
121 }
122
123 /** Get a single issue by ID. */
124 async get (id) {
125 await this._room.base.update()
126 const entry = await this._room.view.get(`issue:${id}`)
127 return entry ? entry.value : null
128 }
129
130 /**
131 * List issues with optional filtering and sorting.
132 * @param {object} [filters]
133 * @param {'open'|'closed'|'all'} [filters.status='all']
134 * @param {string[]} [filters.labels] - must have ALL listed labels
135 * @param {string} [filters.search] - search in title and body
136 * @param {string} [filters.author] - filter by author userId
137 * @param {'newest'|'oldest'|'recently-updated'} [filters.sort='newest']
138 * @param {number} [filters.limit=0]
139 * @param {number} [filters.offset=0]
140 */
141 async list (filters = {}) {
142 const {
143 status = 'all',
144 labels = [],
145 search = '',
146 author = '',
147 sort = 'newest',
148 limit = 0,
149 offset = 0
150 } = filters
151
152 await this._room.base.update()
153 const issues = []
154
155 for await (const entry of this._room.view.createReadStream({
156 gt: 'issue:', lt: 'issue:\xff'
157 })) {
158 const issue = entry.value
159
160 if (status !== 'all' && issue.status !== status) continue
161
162 if (labels.length > 0) {
163 const issueLabels = issue.labels || []
164 const hasAll = labels.every(l => {
165 const normalized = l.toLowerCase().replace(/\s+/g, '-')
166 return issueLabels.includes(normalized)
167 })
168 if (!hasAll) continue
169 }
170
171 if (search) {
172 const s = search.toLowerCase()
173 const titleMatch = issue.title && issue.title.toLowerCase().includes(s)
174 const bodyMatch = issue.body && issue.body.toLowerCase().includes(s)
175 if (!titleMatch && !bodyMatch) continue
176 }
177
178 if (author && issue.author !== author) continue
179 issues.push(issue)
180 }
181
182 switch (sort) {
183 case 'oldest':
184 issues.sort((a, b) => a.id - b.id)
185 break
186 case 'recently-updated':
187 issues.sort((a, b) => (b.updatedAt || b.createdAt) - (a.updatedAt || a.createdAt))
188 break
189 default:
190 issues.sort((a, b) => b.id - a.id)
191 break
192 }
193
194 const total = issues.length
195 const paginated = limit > 0 ? issues.slice(offset, offset + limit) : issues
196
197 if (limit > 0) {
198 return { items: paginated, total, hasMore: offset + limit < total }
199 }
200 return issues
201 }
202
203 /** Issue counts by status. */
204 async counts () {
205 await this._room.base.update()
206 let open = 0
207 let closed = 0
208 for await (const entry of this._room.view.createReadStream({
209 gt: 'issue:', lt: 'issue:\xff'
210 })) {
211 if (entry.value.status === 'closed') closed++
212 else open++
213 }
214 return { open, closed, total: open + closed }
215 }
216
217 // ── Comments ────────────────────────────────────────────────
218
219 async comment (issueId, options) {
220 const { body, author } = options
221 const commentId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
222
223 await this._room.append({
224 type: 'create-comment',
225 issueId,
226 commentId,
227 body,
228 userId: author,
229 timestamp: Date.now()
230 })
231 return commentId
232 }
233
234 async getComments (issueId) {
235 await this._room.base.update()
236 const comments = []
237 for await (const entry of this._room.view.createReadStream({
238 gt: `comment:${issueId}:`, lt: `comment:${issueId}:\xff`
239 })) {
240 comments.push({
241 commentId: entry.key.split(':').slice(2).join(':'),
242 ...entry.value
243 })
244 }
245 return comments
246 }
247
248 async editComment (issueId, commentId, body, userId) {
249 await this._room.append({
250 type: 'update-comment',
251 issueId,
252 commentId,
253 body,
254 userId,
255 timestamp: Date.now()
256 })
257 }
258
259 async deleteComment (issueId, commentId, userId) {
260 await this._room.append({
261 type: 'delete-comment',
262 issueId,
263 commentId,
264 userId,
265 timestamp: Date.now()
266 })
267 }
268
269 // ── Labels ──────────────────────────────────────────────────
270
271 async createLabel (name, color, description, displayName) {
272 await this._room.append({
273 type: 'create-label',
274 name,
275 color: color || '#ccc',
276 description: description || '',
277 displayName: displayName || name
278 })
279 }
280
281 async updateLabel (name, updates) {
282 await this._room.append({ type: 'update-label', name, ...updates })
283 }
284
285 async deleteLabel (name) {
286 await this._room.append({ type: 'delete-label', name })
287 }
288
289 async getLabel (name) {
290 await this._room.base.update()
291 const normalized = name.toLowerCase().replace(/\s+/g, '-')
292 const entry = await this._room.view.get(`label:${normalized}`)
293 return entry ? { name: normalized, ...entry.value } : null
294 }
295
296 async listLabels () {
297 await this._room.base.update()
298 const labels = []
299 for await (const entry of this._room.view.createReadStream({
300 gt: 'label:', lt: 'label:\xff'
301 })) {
302 labels.push({ name: entry.key.slice('label:'.length), ...entry.value })
303 }
304 return labels.sort((a, b) => a.name.localeCompare(b.name))
305 }
306
307 async initializeDefaultLabels () {
308 const existing = await this.listLabels()
309 if (existing.length > 0) return existing
310 for (const label of DEFAULT_LABELS) {
311 await this.createLabel(label.name, label.color, label.description, label.name)
312 }
313 return this.listLabels()
314 }
315
316 async addLabels (id, labelNames, userId) {
317 const issue = await this.get(id)
318 if (!issue) throw new Error('Issue not found')
319
320 const currentLabels = issue.labels || []
321 const newLabels = [...currentLabels]
322 for (const labelName of labelNames) {
323 const normalized = labelName.toLowerCase().replace(/\s+/g, '-')
324 const label = await this.getLabel(normalized)
325 if (!label) throw new Error(`Label "${labelName}" not found`)
326 if (!newLabels.includes(normalized)) newLabels.push(normalized)
327 }
328 await this.update(id, { labels: newLabels }, userId)
329 return this.get(id)
330 }
331
332 async removeLabels (id, labelNames, userId) {
333 const issue = await this.get(id)
334 if (!issue) throw new Error('Issue not found')
335 const toRemove = new Set(labelNames.map(l => l.toLowerCase().replace(/\s+/g, '-')))
336 const newLabels = (issue.labels || []).filter(l => !toRemove.has(l))
337 await this.update(id, { labels: newLabels }, userId)
338 return this.get(id)
339 }
340
341 async setLabels (id, labelNames, userId) {
342 const normalized = []
343 for (const labelName of labelNames) {
344 const n = labelName.toLowerCase().replace(/\s+/g, '-')
345 const label = await this.getLabel(n)
346 if (!label) throw new Error(`Label "${labelName}" not found`)
347 normalized.push(n)
348 }
349 await this.update(id, { labels: normalized }, userId)
350 return this.get(id)
351 }
352
353 // ── Refs (optional, for git integration) ────────────────────
354
355 async updateRef (ref, hash, userId) {
356 await this._room.append({ type: 'update-ref', ref, hash, userId, timestamp: Date.now() })
357 }
358
359 async deleteRef (ref, userId) {
360 await this._room.append({ type: 'delete-ref', ref, userId, timestamp: Date.now() })
361 }
362
363 async getRef (ref) {
364 await this._room.base.update()
365 const entry = await this._room.view.get(`ref:${ref}`)
366 return entry ? entry.value : null
367 }
368
369 async listRefs () {
370 await this._room.base.update()
371 const refs = []
372 for await (const entry of this._room.view.createReadStream({
373 gt: 'ref:', lt: 'ref:\xff'
374 })) {
375 refs.push({ ref: entry.key.slice('ref:'.length), ...entry.value })
376 }
377 return refs
378 }
379
380 // ── Meta ────────────────────────────────────────────────────
381
382 async setMeta (key, value) {
383 await this._room.append({ type: 'set-meta', key, value })
384 }
385
386 async getMeta (key) {
387 await this._room.base.update()
388 const entry = await this._room.view.get(`meta:${key}`)
389 return entry ? entry.value.value : null
390 }
391}
lib/schema.js +189 -0
……@@ -0,0 +1,189 @@
1/**
2 * issuesSchema — hyper-rooms schema for issue tracking
3 *
4 * Op types (writer ops are handled by Room):
5 * create-issue, update-issue
6 * create-comment, update-comment, delete-comment
7 * create-label, update-label, delete-label
8 * update-ref, delete-ref (optional, for git integration)
9 * set-meta
10 *
11 * View schema (Hyperbee keys):
12 * issue:{id} → { id, title, body, author, status, labels, images, createdAt, updatedAt }
13 * comment:{issueId}:{id} → { body, author, createdAt, editedAt?, editedBy? }
14 * label:{name} → { color, description, displayName }
15 * ref:{refPath} → { hash, updatedBy, timestamp }
16 * meta:{key} → { value }
17 * counter:issues → { next: number }
18 */
19
20import { jsonSchema } from 'hyper-rooms'
21
22export const issuesSchema = jsonSchema({
23 validate (op) {
24 switch (op.type) {
25 case 'create-issue':
26 if (typeof op.id !== 'number') throw new Error('create-issue requires numeric id')
27 if (!op.title) throw new Error('create-issue requires title')
28 if (!op.userId) throw new Error('create-issue requires userId')
29 break
30 case 'update-issue':
31 if (typeof op.id !== 'number') throw new Error('update-issue requires numeric id')
32 if (!op.userId) throw new Error('update-issue requires userId')
33 break
34 case 'create-comment':
35 if (typeof op.issueId !== 'number') throw new Error('create-comment requires numeric issueId')
36 if (!op.commentId) throw new Error('create-comment requires commentId')
37 if (!op.body) throw new Error('create-comment requires body')
38 if (!op.userId) throw new Error('create-comment requires userId')
39 break
40 case 'update-comment':
41 case 'delete-comment':
42 if (typeof op.issueId !== 'number') throw new Error(op.type + ' requires numeric issueId')
43 if (!op.commentId) throw new Error(op.type + ' requires commentId')
44 break
45 case 'create-label':
46 case 'update-label':
47 case 'delete-label':
48 if (!op.name) throw new Error(op.type + ' requires name')
49 break
50 case 'update-ref':
51 if (!op.ref) throw new Error('update-ref requires ref')
52 if (!op.hash) throw new Error('update-ref requires hash')
53 break
54 case 'delete-ref':
55 if (!op.ref) throw new Error('delete-ref requires ref')
56 break
57 case 'set-meta':
58 if (!op.key) throw new Error('set-meta requires key')
59 break
60 }
61 },
62
63 async apply (batch, op, host) {
64 switch (op.type) {
65 case 'create-issue': {
66 await batch.put(`issue:${op.id}`, {
67 id: op.id,
68 title: op.title,
69 body: op.body || '',
70 author: op.userId,
71 status: op.status || 'open',
72 labels: op.labels || [],
73 images: op.images || [],
74 createdAt: op.timestamp || Date.now(),
75 updatedAt: op.timestamp || Date.now()
76 })
77 // Bump the auto-increment counter
78 const counter = await batch.get('counter:issues')
79 const currentNext = counter ? counter.value.next : 0
80 if (op.id >= currentNext) {
81 await batch.put('counter:issues', { next: op.id + 1 })
82 }
83 break
84 }
85
86 case 'update-issue': {
87 const entry = await batch.get(`issue:${op.id}`)
88 if (!entry) break
89 const issue = entry.value
90 if (op.title !== undefined) issue.title = op.title
91 if (op.body !== undefined) issue.body = op.body
92 if (op.status !== undefined) issue.status = op.status
93 if (op.labels !== undefined) issue.labels = op.labels
94 if (op.images !== undefined) issue.images = op.images
95 issue.updatedAt = op.timestamp || Date.now()
96 await batch.put(`issue:${op.id}`, issue)
97 break
98 }
99
100 case 'create-comment': {
101 await batch.put(`comment:${op.issueId}:${op.commentId}`, {
102 body: op.body,
103 author: op.userId,
104 createdAt: op.timestamp || Date.now()
105 })
106 // Bump the issue's updatedAt
107 const issueEntry = await batch.get(`issue:${op.issueId}`)
108 if (issueEntry) {
109 issueEntry.value.updatedAt = op.timestamp || Date.now()
110 await batch.put(`issue:${op.issueId}`, issueEntry.value)
111 }
112 break
113 }
114
115 case 'update-comment': {
116 const key = `comment:${op.issueId}:${op.commentId}`
117 const existing = await batch.get(key)
118 if (existing) {
119 existing.value.body = op.body
120 existing.value.editedAt = op.timestamp || Date.now()
121 existing.value.editedBy = op.userId
122 await batch.put(key, existing.value)
123 }
124 break
125 }
126
127 case 'delete-comment': {
128 await batch.del(`comment:${op.issueId}:${op.commentId}`)
129 break
130 }
131
132 case 'create-label': {
133 await batch.put(`label:${op.name}`, {
134 color: op.color || '#ccc',
135 description: op.description || '',
136 displayName: op.displayName || op.name
137 })
138 break
139 }
140
141 case 'update-label': {
142 const labelEntry = await batch.get(`label:${op.name}`)
143 if (labelEntry) {
144 const label = labelEntry.value
145 if (op.color !== undefined) label.color = op.color
146 if (op.description !== undefined) label.description = op.description
147 if (op.displayName !== undefined) label.displayName = op.displayName
148 await batch.put(`label:${op.name}`, label)
149 }
150 break
151 }
152
153 case 'delete-label': {
154 await batch.del(`label:${op.name}`)
155 break
156 }
157
158 case 'update-ref': {
159 await batch.put(`ref:${op.ref}`, {
160 hash: op.hash,
161 updatedBy: op.userId,
162 timestamp: op.timestamp || Date.now()
163 })
164 break
165 }
166
167 case 'delete-ref': {
168 await batch.del(`ref:${op.ref}`)
169 break
170 }
171
172 case 'set-meta': {
173 await batch.put(`meta:${op.key}`, { value: op.value })
174 break
175 }
176 }
177 }
178})
179
180export const DEFAULT_LABELS = [
181 { name: 'bug', color: '#d73a4a', description: 'Something isn\'t working' },
182 { name: 'enhancement', color: '#a2eeef', description: 'New feature or request' },
183 { name: 'documentation', color: '#0075ca', description: 'Improvements or additions to documentation' },
184 { name: 'question', color: '#d876e3', description: 'Further information is requested' },
185 { name: 'help-wanted', color: '#008672', description: 'Extra attention is needed' },
186 { name: 'good-first-issue', color: '#7057ff', description: 'Good for newcomers' },
187 { name: 'duplicate', color: '#cfd3d7', description: 'This issue already exists' },
188 { name: 'wontfix', color: '#ffffff', description: 'This will not be worked on' }
189]
package-lock.json +1428 -0
……@@ -0,0 +1,1428 @@
1{
2 "name": "hyper-issues",
3 "version": "0.2.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "hyper-issues",
9 "version": "0.2.0",
10 "dependencies": {
11 "hyper-rooms": "file:../hyper-rooms"
12 },
13 "devDependencies": {
14 "autobase-test-helpers": "^2.0.0",
15 "brittle": "^3.0.0",
16 "corestore": "^7.0.0"
17 }
18 },
19 "../hyper-rooms": {
20 "version": "0.1.0",
21 "dependencies": {
22 "autobase": "^7.20.0",
23 "b4a": "^1.6.0",
24 "hyperbee": "^2.20.0",
25 "hypercore-crypto": "^3.4.0",
26 "ready-resource": "^1.1.0"
27 },
28 "devDependencies": {
29 "autobase-test-helpers": "^2.0.0",
30 "brittle": "^3.0.0",
31 "corestore": "^7.0.0"
32 }
33 },
34 "node_modules/@hyperswarm/secret-stream": {
35 "version": "6.9.1",
36 "resolved": "https://registry.npmjs.org/@hyperswarm/secret-stream/-/secret-stream-6.9.1.tgz",
37 "integrity": "sha512-xb0S5y3YJwBakD77JOGBHlBxdp63mHClZoXBYoLv+9wH8e054ESKlmQptWqjJK5dv5VMUIVYOJB4MaOpB0JdGw==",
38 "dev": true,
39 "license": "Apache-2.0",
40 "dependencies": {
41 "b4a": "^1.1.0",
42 "hypercore-crypto": "^3.3.1",
43 "noise-curve-ed": "^2.0.1",
44 "noise-handshake": "^4.0.0",
45 "sodium-secretstream": "^1.1.0",
46 "sodium-universal": "^5.0.0",
47 "streamx": "^2.14.0",
48 "timeout-refresh": "^2.0.0",
49 "unslab": "^1.3.0"
50 }
51 },
52 "node_modules/@jridgewell/resolve-uri": {
53 "version": "3.1.2",
54 "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
55 "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
56 "dev": true,
57 "license": "MIT",
58 "engines": {
59 "node": ">=6.0.0"
60 }
61 },
62 "node_modules/@jridgewell/sourcemap-codec": {
63 "version": "1.5.5",
64 "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
65 "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
66 "dev": true,
67 "license": "MIT"
68 },
69 "node_modules/@jridgewell/trace-mapping": {
70 "version": "0.3.31",
71 "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
72 "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
73 "dev": true,
74 "license": "MIT",
75 "dependencies": {
76 "@jridgewell/resolve-uri": "^3.1.0",
77 "@jridgewell/sourcemap-codec": "^1.4.14"
78 }
79 },
80 "node_modules/@types/istanbul-lib-coverage": {
81 "version": "2.0.6",
82 "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
83 "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
84 "dev": true,
85 "license": "MIT"
86 },
87 "node_modules/autobase-test-helpers": {
88 "version": "2.1.3",
89 "resolved": "https://registry.npmjs.org/autobase-test-helpers/-/autobase-test-helpers-2.1.3.tgz",
90 "integrity": "sha512-DYaBLakwMeWv32VcW0StyKZ6mQjA2HV8qRlnIx0XeM93/nw8l13DdiGXZf9EAB14nhKG5drHQIOQrJAiW5BOfg==",
91 "dev": true,
92 "license": "Apache-2.0",
93 "dependencies": {
94 "b4a": "^1.6.4"
95 }
96 },
97 "node_modules/b4a": {
98 "version": "1.8.0",
99 "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
100 "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
101 "dev": true,
102 "license": "Apache-2.0",
103 "peerDependencies": {
104 "react-native-b4a": "*"
105 },
106 "peerDependenciesMeta": {
107 "react-native-b4a": {
108 "optional": true
109 }
110 }
111 },
112 "node_modules/bare-abort": {
113 "version": "2.0.13",
114 "resolved": "https://registry.npmjs.org/bare-abort/-/bare-abort-2.0.13.tgz",
115 "integrity": "sha512-zdc8l88eB11Jsz5rDd6sCAgv2kUFXgdrZWoMlgU6JMkfAi1/uuGFC3IEHswKbIRQTk5H3T5CMuechsXYxiaHlQ==",
116 "dev": true,
117 "license": "Apache-2.0"
118 },
119 "node_modules/bare-addon-resolve": {
120 "version": "1.10.0",
121 "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.10.0.tgz",
122 "integrity": "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA==",
123 "dev": true,
124 "license": "Apache-2.0",
125 "dependencies": {
126 "bare-module-resolve": "^1.10.0",
127 "bare-semver": "^1.0.0"
128 },
129 "peerDependencies": {
130 "bare-url": "*"
131 },
132 "peerDependenciesMeta": {
133 "bare-url": {
134 "optional": true
135 }
136 }
137 },
138 "node_modules/bare-ansi-escapes": {
139 "version": "2.2.3",
140 "resolved": "https://registry.npmjs.org/bare-ansi-escapes/-/bare-ansi-escapes-2.2.3.tgz",
141 "integrity": "sha512-02ES4/E2RbrtZSnHJ9LntBhYkLA6lPpSEeP8iqS3MccBIVhVBlEmruF1I7HZqx5Q8aiTeYfQVeqmrU9YO2yYoQ==",
142 "dev": true,
143 "license": "Apache-2.0",
144 "dependencies": {
145 "bare-stream": "^2.6.5"
146 },
147 "peerDependencies": {
148 "bare-buffer": "*"
149 },
150 "peerDependenciesMeta": {
151 "bare-buffer": {
152 "optional": true
153 }
154 }
155 },
156 "node_modules/bare-assert": {
157 "version": "1.2.0",
158 "resolved": "https://registry.npmjs.org/bare-assert/-/bare-assert-1.2.0.tgz",
159 "integrity": "sha512-c6uvgvTJBspTDxtVnPgrBKmLgcpW3Fp72NVKDLg6oT4QjQbhGtvrkHMhGYMK1sh4vjBHOBmuUalyt9hSzV37fQ==",
160 "dev": true,
161 "license": "Apache-2.0",
162 "dependencies": {
163 "bare-inspect": "^3.1.2"
164 }
165 },
166 "node_modules/bare-bundle": {
167 "version": "1.10.0",
168 "resolved": "https://registry.npmjs.org/bare-bundle/-/bare-bundle-1.10.0.tgz",
169 "integrity": "sha512-4LVlnJAHr00Hh6Vu6ZUJS38rcEtJT3b3vChXSsBsJ2mk1TN0lQ+gzd+Dw5L0aV7uqDZv84smuwW+O02X7PfDlw==",
170 "dev": true,
171 "license": "Apache-2.0",
172 "peerDependencies": {
173 "bare-buffer": "*",
174 "bare-url": "*"
175 },
176 "peerDependenciesMeta": {
177 "bare-buffer": {
178 "optional": true
179 },
180 "bare-url": {
181 "optional": true
182 }
183 }
184 },
185 "node_modules/bare-cov": {
186 "version": "1.2.1",
187 "resolved": "https://registry.npmjs.org/bare-cov/-/bare-cov-1.2.1.tgz",
188 "integrity": "sha512-SHNqb/WG9udUEj8m5qGSehoUQ9o0Xzv8nrf4HIdmG+KNU/XdhPLhTP5VPrHnSAMAXhAzEB4Q+2IYZ0x9qPzN1g==",
189 "dev": true,
190 "license": "Apache-2.0",
191 "dependencies": {
192 "bare-fs": "^4.1.2",
193 "bare-inspector": "^4.0.1",
194 "bare-path": "^3.0.0",
195 "bare-process": "^4.2.1",
196 "bare-url": "^2.1.5",
197 "bare-v8-to-istanbul": "^1.0.2",
198 "picomatch": "^4.0.2",
199 "which-runtime": "^1.2.1"
200 }
201 },
202 "node_modules/bare-crypto": {
203 "version": "1.13.4",
204 "resolved": "https://registry.npmjs.org/bare-crypto/-/bare-crypto-1.13.4.tgz",
205 "integrity": "sha512-JiCZ5l2YOG1y8J7yy1BCAKTCZrPnHLb7pDRIdurBTOn5oIwBQDIv8iH5Pl2V85vzjl1NZXRfNY4HZLsE942jJA==",
206 "dev": true,
207 "license": "Apache-2.0",
208 "dependencies": {
209 "bare-assert": "^1.2.0",
210 "bare-stream": "^2.6.3"
211 },
212 "peerDependencies": {
213 "bare-buffer": "*"
214 },
215 "peerDependenciesMeta": {
216 "bare-buffer": {
217 "optional": true
218 }
219 }
220 },
221 "node_modules/bare-debug-log": {
222 "version": "2.0.0",
223 "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz",
224 "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==",
225 "dev": true,
226 "license": "Apache-2.0",
227 "dependencies": {
228 "bare-os": "^3.0.1"
229 }
230 },
231 "node_modules/bare-dns": {
232 "version": "2.1.4",
233 "resolved": "https://registry.npmjs.org/bare-dns/-/bare-dns-2.1.4.tgz",
234 "integrity": "sha512-abwjHmpWqSRNB7V5615QxPH92L71AVzFm/kKTs8VYiNTAi2xVdonpv0BjJ0hwXLwomoW+xsSOPjW6PZPO14asg==",
235 "dev": true,
236 "license": "Apache-2.0",
237 "engines": {
238 "bare": ">=1.7.0"
239 }
240 },
241 "node_modules/bare-encoding": {
242 "version": "1.0.3",
243 "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.3.tgz",
244 "integrity": "sha512-Kqf+t/azs13lUeyK4Tb7ha4wdLRXKWCXQ8w1rVmt7KtoPCPdHD/Xwt7LBIsCSwwGglrcmblo5VOLa5avkJqULA==",
245 "dev": true,
246 "license": "Apache-2.0",
247 "peerDependencies": {
248 "bare-buffer": "*"
249 },
250 "peerDependenciesMeta": {
251 "bare-buffer": {
252 "optional": true
253 }
254 }
255 },
256 "node_modules/bare-env": {
257 "version": "3.0.0",
258 "resolved": "https://registry.npmjs.org/bare-env/-/bare-env-3.0.0.tgz",
259 "integrity": "sha512-0u964P5ZLAxTi+lW4Kjp7YRJQ5gZr9ycYOtjLxsSrupgMz3sn5Z9n4SH/JIifHwvadsf1brA2JAjP+9IOWwTiw==",
260 "dev": true,
261 "license": "Apache-2.0",
262 "dependencies": {
263 "bare-os": "^3.0.1"
264 }
265 },
266 "node_modules/bare-events": {
267 "version": "2.8.2",
268 "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
269 "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
270 "dev": true,
271 "license": "Apache-2.0",
272 "peerDependencies": {
273 "bare-abort-controller": "*"
274 },
275 "peerDependenciesMeta": {
276 "bare-abort-controller": {
277 "optional": true
278 }
279 }
280 },
281 "node_modules/bare-format": {
282 "version": "1.0.2",
283 "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.2.tgz",
284 "integrity": "sha512-GswdhnOnP9QtwRbrf4wLApw5widkaLMsLe2XOs35fQD2YfEN1ApoGka+cZ7PfvzxMgfYXmMhj/2OGlVn5/Dxgw==",
285 "dev": true,
286 "license": "Apache-2.0",
287 "dependencies": {
288 "bare-inspect": "^3.0.0"
289 }
290 },
291 "node_modules/bare-fs": {
292 "version": "4.7.0",
293 "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.0.tgz",
294 "integrity": "sha512-xzqKsCFxAek9aezYhjJuJRXBIaYlg/0OGDTZp+T8eYmYMlm66cs6cYko02drIyjN2CBbi+I6L7YfXyqpqtKRXA==",
295 "dev": true,
296 "license": "Apache-2.0",
297 "dependencies": {
298 "bare-events": "^2.5.4",
299 "bare-path": "^3.0.0",
300 "bare-stream": "^2.6.4",
301 "bare-url": "^2.2.2",
302 "fast-fifo": "^1.3.2"
303 },
304 "engines": {
305 "bare": ">=1.16.0"
306 },
307 "peerDependencies": {
308 "bare-buffer": "*"
309 },
310 "peerDependenciesMeta": {
311 "bare-buffer": {
312 "optional": true
313 }
314 }
315 },
316 "node_modules/bare-hrtime": {
317 "version": "2.1.1",
318 "resolved": "https://registry.npmjs.org/bare-hrtime/-/bare-hrtime-2.1.1.tgz",
319 "integrity": "sha512-VMb3tHo05gsnbu3OXTmkDiwTjMlOsbQmKoysKqKEyR09m77TuDrYFbj3Q5GGk10dAKsUHrnXmwCaeJqzVpB5ZA==",
320 "dev": true,
321 "license": "Apache-2.0"
322 },
323 "node_modules/bare-http-parser": {
324 "version": "1.1.3",
325 "resolved": "https://registry.npmjs.org/bare-http-parser/-/bare-http-parser-1.1.3.tgz",
326 "integrity": "sha512-+dhVvQi6brHq14L/XHNRQ+TLuVE76VjRmMt61wVEtS+Od8xUslfMHWJN/ZjIIt3RtTG6vPuA+x9cOh7KrkBJsA==",
327 "dev": true,
328 "license": "Apache-2.0"
329 },
330 "node_modules/bare-http1": {
331 "version": "4.5.6",
332 "resolved": "https://registry.npmjs.org/bare-http1/-/bare-http1-4.5.6.tgz",
333 "integrity": "sha512-31OAwMkSU+z1VuUOCk65hx3aWQgzCfH/zQ6LGxbJtmiy2Czsw0+uvOBM9YkqaL6zUSTSYG2pLbL0v/TjME3Buw==",
334 "dev": true,
335 "license": "Apache-2.0",
336 "dependencies": {
337 "bare-events": "^2.6.0",
338 "bare-http-parser": "^1.1.1",
339 "bare-stream": "^2.10.0",
340 "bare-tcp": "^2.2.0"
341 },
342 "peerDependencies": {
343 "bare-buffer": "*",
344 "bare-url": "*"
345 },
346 "peerDependenciesMeta": {
347 "bare-buffer": {
348 "optional": true
349 },
350 "bare-url": {
351 "optional": true
352 }
353 }
354 },
355 "node_modules/bare-https": {
356 "version": "2.1.3",
357 "resolved": "https://registry.npmjs.org/bare-https/-/bare-https-2.1.3.tgz",
358 "integrity": "sha512-0TI/mJXQGYXmG7UUyWEG+KCJusayIAQLywUjFAskDoKuxfqVnGF+M/mTMrEV8J64DaIdU+5x761FvgmT7M68tA==",
359 "dev": true,
360 "license": "Apache-2.0",
361 "dependencies": {
362 "bare-http1": "^4.4.0",
363 "bare-tcp": "^2.2.0",
364 "bare-tls": "^2.0.0"
365 }
366 },
367 "node_modules/bare-inspect": {
368 "version": "3.1.4",
369 "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz",
370 "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==",
371 "dev": true,
372 "license": "Apache-2.0",
373 "dependencies": {
374 "bare-ansi-escapes": "^2.1.0",
375 "bare-type": "^1.0.0"
376 },
377 "engines": {
378 "bare": ">=1.18.0"
379 }
380 },
381 "node_modules/bare-inspector": {
382 "version": "4.0.3",
383 "resolved": "https://registry.npmjs.org/bare-inspector/-/bare-inspector-4.0.3.tgz",
384 "integrity": "sha512-yYk6+7tTVKJKHOja5+QBn989ETv8oVsmwoH0nCM+QYC6w5MC03GC4aQNK+lipAZSz1EaAj42WZ8Wewjpy0L9TA==",
385 "dev": true,
386 "license": "Apache-2.0",
387 "dependencies": {
388 "bare-events": "^2.1.0",
389 "bare-http1": "^4.0.0",
390 "bare-stream": "^2.0.0",
391 "bare-url": "^2.0.0",
392 "bare-ws": "^2.0.0"
393 },
394 "engines": {
395 "bare": ">=1.2.0"
396 },
397 "peerDependencies": {
398 "bare-tcp": "*"
399 },
400 "peerDependenciesMeta": {
401 "bare-tcp": {
402 "optional": true
403 }
404 }
405 },
406 "node_modules/bare-module": {
407 "version": "6.1.3",
408 "resolved": "https://registry.npmjs.org/bare-module/-/bare-module-6.1.3.tgz",
409 "integrity": "sha512-5XWsVHsvtWMH4tK4DQWgpNTV0t/sg3ZrAaQLIxrwjrS5+u8Q9vEgc/zQ4QaDPWDse/y/5h+d+YG1Q0JfSMt0zA==",
410 "dev": true,
411 "license": "Apache-2.0",
412 "dependencies": {
413 "bare-bundle": "^1.3.0",
414 "bare-module-lexer": "^1.0.0",
415 "bare-module-resolve": "^1.8.0",
416 "bare-path": "^3.0.0",
417 "bare-url": "^2.0.1"
418 },
419 "engines": {
420 "bare": ">=1.23.0"
421 },
422 "peerDependencies": {
423 "bare-buffer": "*"
424 },
425 "peerDependenciesMeta": {
426 "bare-buffer": {
427 "optional": true
428 }
429 }
430 },
431 "node_modules/bare-module-lexer": {
432 "version": "1.4.7",
433 "resolved": "https://registry.npmjs.org/bare-module-lexer/-/bare-module-lexer-1.4.7.tgz",
434 "integrity": "sha512-0klU4eMsjh/wcxi8FdHmNom2j2F4kmkXOhyJFL9qTaSFp2lE3m6BtbKgMHY8R5miqC9r8/IfA8wzXnC5Os14WA==",
435 "dev": true,
436 "license": "Apache-2.0",
437 "dependencies": {
438 "require-addon": "^1.0.2"
439 },
440 "peerDependencies": {
441 "bare-buffer": "*"
442 },
443 "peerDependenciesMeta": {
444 "bare-buffer": {
445 "optional": true
446 }
447 }
448 },
449 "node_modules/bare-module-resolve": {
450 "version": "1.12.1",
451 "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.1.tgz",
452 "integrity": "sha512-hbmAPyFpEq8FoZMd5sFO3u6MC5feluWoGE8YKlA8fCrl6mNtx68Wjg4DTiDJcqRJaovTvOYKfYngoBUnbaT7eg==",
453 "dev": true,
454 "license": "Apache-2.0",
455 "dependencies": {
456 "bare-semver": "^1.0.0"
457 },
458 "peerDependencies": {
459 "bare-url": "*"
460 },
461 "peerDependenciesMeta": {
462 "bare-url": {
463 "optional": true
464 }
465 }
466 },
467 "node_modules/bare-net": {
468 "version": "2.3.1",
469 "resolved": "https://registry.npmjs.org/bare-net/-/bare-net-2.3.1.tgz",
470 "integrity": "sha512-MypSqDKpDU2Xt7FIfazn5yGvRnV09gFcIPHGWstW0gxuzA4tucTcwJSZeos97C4F89vtU5oGwXDN/HrGN6Y4Jw==",
471 "dev": true,
472 "license": "Apache-2.0",
473 "dependencies": {
474 "bare-events": "^2.2.2",
475 "bare-pipe": "^4.0.0",
476 "bare-stream": "^2.0.0",
477 "bare-tcp": "^2.0.0"
478 }
479 },
480 "node_modules/bare-os": {
481 "version": "3.8.7",
482 "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz",
483 "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==",
484 "dev": true,
485 "license": "Apache-2.0",
486 "engines": {
487 "bare": ">=1.14.0"
488 }
489 },
490 "node_modules/bare-path": {
491 "version": "3.0.0",
492 "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
493 "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
494 "dev": true,
495 "license": "Apache-2.0",
496 "dependencies": {
497 "bare-os": "^3.0.1"
498 }
499 },
500 "node_modules/bare-pipe": {
501 "version": "4.1.5",
502 "resolved": "https://registry.npmjs.org/bare-pipe/-/bare-pipe-4.1.5.tgz",
503 "integrity": "sha512-6OfxaG8JSkRh3Gc4hzHRsxNt+yu2PpN7lrv1V+T78GdknWQkVGwiEvu4m+1nbfk8cMVQ0TGxRvQ90XA4rhnTuw==",
504 "dev": true,
505 "license": "Apache-2.0",
506 "dependencies": {
507 "bare-events": "^2.0.0",
508 "bare-stream": "^2.0.0"
509 },
510 "engines": {
511 "bare": ">=1.16.0"
512 }
513 },
514 "node_modules/bare-process": {
515 "version": "4.4.1",
516 "resolved": "https://registry.npmjs.org/bare-process/-/bare-process-4.4.1.tgz",
517 "integrity": "sha512-JfcTtymq6akqM2bdyTsP4A4GYJceBzb91k/ECmFps75uFf6uO33SM1bOZsRAqyRXExabsQJLn5S41NBl8HTM4A==",
518 "dev": true,
519 "license": "Apache-2.0",
520 "dependencies": {
521 "bare-abort": "^2.0.13",
522 "bare-env": "^3.0.0",
523 "bare-events": "^2.3.1",
524 "bare-hrtime": "^2.0.0",
525 "bare-os": "^3.7.1",
526 "bare-signals": "^4.0.0",
527 "bare-stdio": "^1.0.1"
528 }
529 },
530 "node_modules/bare-semver": {
531 "version": "1.0.3",
532 "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.3.tgz",
533 "integrity": "sha512-HS/A30bi2+PiRJfU6R4+Kp+6KeLSCSByjYM2iiobOKzLAvtu1CT+S8xWfiU7wz0erknjkUoC+yXy108tzIuP5Q==",
534 "dev": true,
535 "license": "Apache-2.0"
536 },
537 "node_modules/bare-signals": {
538 "version": "4.2.0",
539 "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz",
540 "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==",
541 "dev": true,
542 "license": "Apache-2.0",
543 "dependencies": {
544 "bare-events": "^2.5.3",
545 "bare-os": "^3.3.1"
546 },
547 "engines": {
548 "bare": ">=1.7.0"
549 }
550 },
551 "node_modules/bare-stdio": {
552 "version": "1.0.2",
553 "resolved": "https://registry.npmjs.org/bare-stdio/-/bare-stdio-1.0.2.tgz",
554 "integrity": "sha512-3WJDqtvVGP4f+j68kyEC05umOYNwKJ1xG+YAXL8yZ605WgNqiRhVaFq+mVIhBt2eKNp7pa5vCQdhOt1pNh79SA==",
555 "dev": true,
556 "license": "Apache-2.0",
557 "dependencies": {
558 "bare-fs": "^4.5.2",
559 "bare-pipe": "^4.1.5",
560 "bare-tty": "^5.0.3"
561 }
562 },
563 "node_modules/bare-stream": {
564 "version": "2.13.0",
565 "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
566 "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
567 "dev": true,
568 "license": "Apache-2.0",
569 "dependencies": {
570 "streamx": "^2.25.0",
571 "teex": "^1.0.1"
572 },
573 "peerDependencies": {
574 "bare-abort-controller": "*",
575 "bare-buffer": "*",
576 "bare-events": "*"
577 },
578 "peerDependenciesMeta": {
579 "bare-abort-controller": {
580 "optional": true
581 },
582 "bare-buffer": {
583 "optional": true
584 },
585 "bare-events": {
586 "optional": true
587 }
588 }
589 },
590 "node_modules/bare-stylize": {
591 "version": "0.0.1",
592 "resolved": "https://registry.npmjs.org/bare-stylize/-/bare-stylize-0.0.1.tgz",
593 "integrity": "sha512-l3MjmIl476bWijYWf3RbE+osl4iuXSOMudzp0vAqzIK7gPgn/+G3oAxp8Oin9CFF911KBP0LO9kts8Ci8mGZaQ==",
594 "dev": true,
595 "license": "Apache-2.0",
596 "dependencies": {
597 "bare-ansi-escapes": "^2.2.3",
598 "bare-process": "^4.2.1"
599 }
600 },
601 "node_modules/bare-subprocess": {
602 "version": "5.2.3",
603 "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.2.3.tgz",
604 "integrity": "sha512-07wwswlV7M3sC9IykbZRZ/jHAkrXFWVLqdBWGv1y0ojCimtRD9hGwxdHmR5FUFmDUZLNsBmTYJNQqgio5+A85Q==",
605 "dev": true,
606 "license": "Apache-2.0",
607 "dependencies": {
608 "bare-env": "^3.0.0",
609 "bare-events": "^2.5.4",
610 "bare-os": "^3.0.1",
611 "bare-pipe": "^4.0.0",
612 "bare-url": "^2.2.2"
613 },
614 "engines": {
615 "bare": ">=1.7.0"
616 },
617 "peerDependencies": {
618 "bare-buffer": "*"
619 },
620 "peerDependenciesMeta": {
621 "bare-buffer": {
622 "optional": true
623 }
624 }
625 },
626 "node_modules/bare-tcp": {
627 "version": "2.2.7",
628 "resolved": "https://registry.npmjs.org/bare-tcp/-/bare-tcp-2.2.7.tgz",
629 "integrity": "sha512-rjpqNQ2cOCkNo3NeYA/W4GTK3DRkl8sDHO3uos+AEswUjLC8XXMQF8WrJCSjlIowCbS6NUVxKE92X5RGXjyefg==",
630 "dev": true,
631 "license": "Apache-2.0",
632 "dependencies": {
633 "bare-dns": "^2.0.4",
634 "bare-events": "^2.5.4",
635 "bare-stream": "^2.6.4"
636 },
637 "engines": {
638 "bare": ">=1.16.0"
639 }
640 },
641 "node_modules/bare-tls": {
642 "version": "2.2.3",
643 "resolved": "https://registry.npmjs.org/bare-tls/-/bare-tls-2.2.3.tgz",
644 "integrity": "sha512-dPYBGEXtgLceRFMfGaF2/rqR86/xMxMyrM9ootO/gaRKL/z2uNHJs7aP7IOBtnJF8eUCt5qwMzbKfrKgDIxPLg==",
645 "dev": true,
646 "license": "Apache-2.0",
647 "dependencies": {
648 "bare-net": "^2.0.1",
649 "bare-stream": "^2.6.4"
650 },
651 "engines": {
652 "bare": ">=1.7.0"
653 }
654 },
655 "node_modules/bare-tty": {
656 "version": "5.1.0",
657 "resolved": "https://registry.npmjs.org/bare-tty/-/bare-tty-5.1.0.tgz",
658 "integrity": "sha512-EZLvW4A+XiJgI3TW+e1pMME9PIJsfEXe/DA/WSKzIkq/v7Yarpv/rvG6Z5pGnpo4V/Bd+qopwnCLSR71hMMBYA==",
659 "dev": true,
660 "license": "Apache-2.0",
661 "dependencies": {
662 "bare-events": "^2.2.0",
663 "bare-signals": "^4.0.0",
664 "bare-stream": "^2.0.0"
665 },
666 "engines": {
667 "bare": ">=1.16.0"
668 }
669 },
670 "node_modules/bare-type": {
671 "version": "1.1.0",
672 "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz",
673 "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==",
674 "dev": true,
675 "license": "Apache-2.0",
676 "engines": {
677 "bare": ">=1.2.0"
678 }
679 },
680 "node_modules/bare-url": {
681 "version": "2.4.0",
682 "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
683 "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
684 "dev": true,
685 "license": "Apache-2.0",
686 "peer": true,
687 "dependencies": {
688 "bare-path": "^3.0.0"
689 }
690 },
691 "node_modules/bare-utils": {
692 "version": "1.6.0",
693 "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.6.0.tgz",
694 "integrity": "sha512-WhQEIkkAxkSnW7u1QgrI0AfNm5JpMruETXeYsb5qnkBJ0TTfNKygZmsh6rkoHBANaV+C/7Jed7bJP9OmEHG7rQ==",
695 "dev": true,
696 "license": "Apache-2.0",
697 "dependencies": {
698 "bare-debug-log": "^2.0.0",
699 "bare-encoding": "^1.0.0",
700 "bare-format": "^1.0.0",
701 "bare-inspect": "^3.0.0",
702 "bare-stylize": "^0.0.1",
703 "bare-type": "^1.0.6"
704 }
705 },
706 "node_modules/bare-v8-to-istanbul": {
707 "version": "1.0.2",
708 "resolved": "https://registry.npmjs.org/bare-v8-to-istanbul/-/bare-v8-to-istanbul-1.0.2.tgz",
709 "integrity": "sha512-CM0wPgyQtXTQ7h4q8NjbTZ/FH5he0Jkiwgpq0lFZZA5tkVZzmQ/9ul4R7E+cNlGZJIZlQZ9v4CwbgVaFG3zOAA==",
710 "dev": true,
711 "license": "Apache-2.0",
712 "dependencies": {
713 "bare-assert": "^1.0.2",
714 "bare-fs": "^4.1.2",
715 "bare-module": "^6.1.2",
716 "bare-path": "^3.0.0",
717 "bare-process": "^4.2.0",
718 "bare-url": "^2.1.5",
719 "bare-utils": "^1.2.0",
720 "v8-to-istanbul": "^9.3.0",
721 "which-runtime": "^1.2.1"
722 }
723 },
724 "node_modules/bare-ws": {
725 "version": "2.1.0",
726 "resolved": "https://registry.npmjs.org/bare-ws/-/bare-ws-2.1.0.tgz",
727 "integrity": "sha512-2gEWlPK9iyBchACdIY6oQXgmDz3KLrChdwrPgmU3IVXOFTnxTqSUT27WE/+izd4QojHj/SsqDkQiD2HDvuTdAA==",
728 "dev": true,
729 "license": "Apache-2.0",
730 "dependencies": {
731 "bare-crypto": "^1.2.0",
732 "bare-events": "^2.3.1",
733 "bare-http1": "^4.0.0",
734 "bare-https": "^2.0.0",
735 "bare-stream": "^2.1.2"
736 },
737 "peerDependencies": {
738 "bare-buffer": "*",
739 "bare-url": "*"
740 },
741 "peerDependenciesMeta": {
742 "bare-buffer": {
743 "optional": true
744 },
745 "bare-url": {
746 "optional": true
747 }
748 }
749 },
750 "node_modules/big-sparse-array": {
751 "version": "1.0.3",
752 "resolved": "https://registry.npmjs.org/big-sparse-array/-/big-sparse-array-1.0.3.tgz",
753 "integrity": "sha512-6RjV/3mSZORlMdpUaQ6rUSpG637cZm0//E54YYGtQg1c1O+AbZP8UTdJ/TchsDZcTVLmyWZcseBfp2HBeXUXOQ==",
754 "dev": true,
755 "license": "MIT"
756 },
757 "node_modules/brittle": {
758 "version": "3.19.1",
759 "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz",
760 "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==",
761 "dev": true,
762 "license": "Apache-2.0",
763 "dependencies": {
764 "b4a": "^1.6.0",
765 "bare-assert": "^1.0.2",
766 "bare-cov": "^1.1.0",
767 "bare-fs": "^4.1.6",
768 "bare-os": "^3.6.1",
769 "bare-path": "^3.0.0",
770 "bare-process": "^4.2.1",
771 "bare-subprocess": "^5.0.0",
772 "bare-url": "^2.1.6",
773 "error-stack-parser": "^2.1.4",
774 "globbie": "^1.0.2",
775 "paparam": "^1.6.2",
776 "same-object": "^1.0.2",
777 "test-tmp": "^1.4.0",
778 "tmatch": "^5.0.0"
779 },
780 "bin": {
781 "brittle": "bin/node.js",
782 "brittle-bare": "bin/bare.js",
783 "brittle-node": "bin/node.js",
784 "brittle-pear": "bin/pear.js"
785 }
786 },
787 "node_modules/compact-encoding": {
788 "version": "2.19.2",
789 "resolved": "https://registry.npmjs.org/compact-encoding/-/compact-encoding-2.19.2.tgz",
790 "integrity": "sha512-/YjhHQE/5L4F7l5Bht69dRbP9RV6zoJPeowi8bMKQxNKe3Nh6hOY8pBGoVE9fz5GaWfEd8fWJ2aU9sB4KZuMYg==",
791 "dev": true,
792 "license": "Apache-2.0",
793 "dependencies": {
794 "b4a": "^1.3.0"
795 }
796 },
797 "node_modules/convert-source-map": {
798 "version": "2.0.0",
799 "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
800 "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
801 "dev": true,
802 "license": "MIT"
803 },
804 "node_modules/corestore": {
805 "version": "7.9.2",
806 "resolved": "https://registry.npmjs.org/corestore/-/corestore-7.9.2.tgz",
807 "integrity": "sha512-dyIktVSVLu0skZ1UM4yODbNbZ46dBpTHypzwlwET1btVuS3YIf66Zz8Tx8h3Xa8nbLO2USGqrYtvPYzSRj2wCA==",
808 "dev": true,
809 "license": "MIT",
810 "dependencies": {
811 "b4a": "^1.6.7",
812 "hypercore": "^11.19.0",
813 "hypercore-crypto": "^3.4.2",
814 "hypercore-errors": "^1.4.0",
815 "hypercore-id-encoding": "^1.3.0",
816 "ready-resource": "^1.1.1",
817 "sodium-universal": "^5.0.1",
818 "which-runtime": "^1.2.1"
819 }
820 },
821 "node_modules/device-file": {
822 "version": "2.3.1",
823 "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.3.1.tgz",
824 "integrity": "sha512-bmON44lwxJPle9N2OcH4tqM44pMGZKT8G6OkzXkz0urvqQ9LKkoQdTS6w0ztYfmLdUE27R4UUmx0+y2gEz4Jug==",
825 "dev": true,
826 "license": "Apache-2.0",
827 "dependencies": {
828 "b4a": "^1.6.7",
829 "bare-fs": "^4.0.1",
830 "bare-path": "^3.0.0",
831 "fd-lock": "^2.1.0",
832 "fs-native-extensions": "^1.4.0",
833 "ready-resource": "^1.2.0"
834 }
835 },
836 "node_modules/error-stack-parser": {
837 "version": "2.1.4",
838 "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
839 "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
840 "dev": true,
841 "license": "MIT",
842 "dependencies": {
843 "stackframe": "^1.3.4"
844 }
845 },
846 "node_modules/events-universal": {
847 "version": "1.0.1",
848 "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
849 "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
850 "dev": true,
851 "license": "Apache-2.0",
852 "dependencies": {
853 "bare-events": "^2.7.0"
854 }
855 },
856 "node_modules/fast-fifo": {
857 "version": "1.3.2",
858 "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
859 "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
860 "dev": true,
861 "license": "MIT"
862 },
863 "node_modules/fd-lock": {
864 "version": "2.1.1",
865 "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz",
866 "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==",
867 "dev": true,
868 "license": "Apache-2.0",
869 "dependencies": {
870 "bare-fs": "^4.5.0",
871 "fs-native-extensions": "^1.4.4",
872 "ready-resource": "^1.2.0",
873 "resource-on-exit": "^1.0.0"
874 }
875 },
876 "node_modules/flat-tree": {
877 "version": "1.13.0",
878 "resolved": "https://registry.npmjs.org/flat-tree/-/flat-tree-1.13.0.tgz",
879 "integrity": "sha512-fT3HIuCPwHhFgJ20QYzDHgUG0zMmFg5cHvFiFo5h+QMSJ28TihsEVY0f8HGliuO+pOzmvjMx1odToeaEWkTnyQ==",
880 "dev": true,
881 "license": "MIT"
882 },
883 "node_modules/fs-native-extensions": {
884 "version": "1.4.5",
885 "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz",
886 "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==",
887 "dev": true,
888 "license": "Apache-2.0",
889 "dependencies": {
890 "require-addon": "^1.1.0",
891 "which-runtime": "^1.2.0"
892 }
893 },
894 "node_modules/generate-object-property": {
895 "version": "2.0.0",
896 "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-2.0.0.tgz",
897 "integrity": "sha512-KwuURPyqn2Mz8DdV29pJwQu0Y7tcsbkULr82eeOcY/ZllFK6I9Wm8dsRByIu7CKWlFi9BdW1b3mcXMp/kQBQsw==",
898 "dev": true,
899 "license": "MIT",
900 "dependencies": {
901 "is-property": "^1.0.0"
902 }
903 },
904 "node_modules/generate-string": {
905 "version": "1.0.1",
906 "resolved": "https://registry.npmjs.org/generate-string/-/generate-string-1.0.1.tgz",
907 "integrity": "sha512-IfTY0dKZM43ACyGvXkbG7De7WY7MxTS5VO6Juhe8oJKpCmrYYXoqp/cJMskkpi0k9H8wuXq0H+eI898/BCqvXg==",
908 "dev": true,
909 "license": "MIT"
910 },
911 "node_modules/globbie": {
912 "version": "1.0.3",
913 "resolved": "https://registry.npmjs.org/globbie/-/globbie-1.0.3.tgz",
914 "integrity": "sha512-hcryJmKcftf82xfBWTWxuUITWIIoYO3ec104V17SMHGFl6VLbm1d1Ju9LX9L+jyJTwICpemX/dmPI/HGYoKr1A==",
915 "dev": true,
916 "license": "Apache-2.0",
917 "dependencies": {
918 "bare-fs": "^4.1.2",
919 "bare-path": "^3.0.0",
920 "bare-process": "^4.2.1",
921 "picomatch": "^4.0.2"
922 }
923 },
924 "node_modules/hyper-rooms": {
925 "resolved": "../hyper-rooms",
926 "link": true
927 },
928 "node_modules/hypercore": {
929 "version": "11.28.1",
930 "resolved": "https://registry.npmjs.org/hypercore/-/hypercore-11.28.1.tgz",
931 "integrity": "sha512-d8jqUilG7Gri8wT+sRdkFTYRPMHGyvBz/NCYHf5rX7+LoivRNRxQm5nWChT7caiUKbcsDak6h7IiEQ5wbD1wCg==",
932 "dev": true,
933 "license": "MIT",
934 "dependencies": {
935 "@hyperswarm/secret-stream": "^6.0.0",
936 "b4a": "^1.1.0",
937 "bare-events": "^2.2.0",
938 "big-sparse-array": "^1.0.3",
939 "compact-encoding": "^2.11.0",
940 "fast-fifo": "^1.3.0",
941 "flat-tree": "^1.9.0",
942 "hypercore-crypto": "^3.2.1",
943 "hypercore-errors": "^1.5.0",
944 "hypercore-id-encoding": "^1.2.0",
945 "hypercore-storage": "^2.8.0",
946 "is-options": "^1.0.1",
947 "nanoassert": "^2.0.0",
948 "protomux": "^3.5.0",
949 "quickbit-universal": "^2.2.0",
950 "random-array-iterator": "^1.0.0",
951 "safety-catch": "^1.0.1",
952 "sodium-universal": "^5.0.1",
953 "streamx": "^2.12.4",
954 "unslab": "^1.3.0",
955 "z32": "^1.0.0"
956 }
957 },
958 "node_modules/hypercore-crypto": {
959 "version": "3.6.1",
960 "resolved": "https://registry.npmjs.org/hypercore-crypto/-/hypercore-crypto-3.6.1.tgz",
961 "integrity": "sha512-ltIz2uDwy9pO/ZGTvqcjzyBkvt6O4cVm4r/nNxh0GFs/RbQtqP/i4wCvLEdmU7ptgtnw7fI67WYD1aHPuv4OVA==",
962 "dev": true,
963 "license": "MIT",
964 "dependencies": {
965 "b4a": "^1.6.6",
966 "compact-encoding": "^2.15.0",
967 "sodium-universal": "^5.0.0"
968 }
969 },
970 "node_modules/hypercore-errors": {
971 "version": "1.5.0",
972 "resolved": "https://registry.npmjs.org/hypercore-errors/-/hypercore-errors-1.5.0.tgz",
973 "integrity": "sha512-5KQ/SuDxsvet+7qWA35Ay6zdD9WyAHQoyWHGcPUTbmJBd300gvNIJoi3oma7kp4TTCSzii6qYumNZe/s0j/saQ==",
974 "dev": true,
975 "license": "Apache-2.0",
976 "dependencies": {
977 "hypercore-id-encoding": "^1.3.0"
978 }
979 },
980 "node_modules/hypercore-id-encoding": {
981 "version": "1.3.0",
982 "resolved": "https://registry.npmjs.org/hypercore-id-encoding/-/hypercore-id-encoding-1.3.0.tgz",
983 "integrity": "sha512-W6sHdGo5h7LXEsoWfKf/KfuROZmZRQDlGqJF2EPHW+noCK66Vvr0+zE6cL0vqQi18s0kQPeN7Sq3QyR0Ytc2VQ==",
984 "dev": true,
985 "license": "Apache-2.0",
986 "dependencies": {
987 "b4a": "^1.5.3",
988 "z32": "^1.0.0"
989 }
990 },
991 "node_modules/hypercore-storage": {
992 "version": "2.8.0",
993 "resolved": "https://registry.npmjs.org/hypercore-storage/-/hypercore-storage-2.8.0.tgz",
994 "integrity": "sha512-s35HllFMYvrkSHvkIIklVFmCI/bRVNO7H3pi4mKvWl0XLyhGGGM+NtJ7fpsh82oPRCmL9XtMRO+pT6LiMAO8MA==",
995 "dev": true,
996 "license": "Apache-2.0",
997 "dependencies": {
998 "b4a": "^1.6.7",
999 "bare-fs": "^4.0.1",
1000 "bare-path": "^3.0.0",
1001 "compact-encoding": "^2.16.0",
1002 "device-file": "^2.1.2",
1003 "flat-tree": "^1.12.1",
1004 "hypercore-crypto": "^3.4.2",
1005 "hyperschema": "^1.7.0",
1006 "index-encoder": "^3.3.2",
1007 "resolve-reject-promise": "^1.0.0",
1008 "rocksdb-native": "^3.11.0",
1009 "scope-lock": "^1.2.4",
1010 "streamx": "^2.21.1"
1011 }
1012 },
1013 "node_modules/hyperschema": {
1014 "version": "1.20.1",
1015 "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.20.1.tgz",
1016 "integrity": "sha512-7gnvaTNUs8FqdrU3NNE6nzhOneGyDlUlpROu9YFlHA61fE8ppKKf2gLZjZjsx/enNt+VFN4ITxy8ijypfyWBLw==",
1017 "dev": true,
1018 "license": "Apache-2.0",
1019 "dependencies": {
1020 "bare-fs": "^4.0.1",
1021 "compact-encoding": "^2.19.0",
1022 "generate-object-property": "^2.0.0",
1023 "generate-string": "^1.0.1"
1024 }
1025 },
1026 "node_modules/index-encoder": {
1027 "version": "3.5.0",
1028 "resolved": "https://registry.npmjs.org/index-encoder/-/index-encoder-3.5.0.tgz",
1029 "integrity": "sha512-idZ1cxtZz2dRV6rUiaP9Xo99UjXbSzjcMacoQmxUMu/A7fEQcNPngvwDJYeWelQUS5XFlY71/or70lKn6XnwbQ==",
1030 "dev": true,
1031 "license": "Apache-2.0",
1032 "dependencies": {
1033 "b4a": "^1.6.4"
1034 }
1035 },
1036 "node_modules/is-options": {
1037 "version": "1.0.2",
1038 "resolved": "https://registry.npmjs.org/is-options/-/is-options-1.0.2.tgz",
1039 "integrity": "sha512-u+Ai74c8Q74aS8BuHwPdI1jptGOT1FQXgCq8/zv0xRuE+wRgSMEJLj8lVO8Zp9BeGb29BXY6AsNPinfqjkr7Fg==",
1040 "dev": true,
1041 "license": "MIT",
1042 "dependencies": {
1043 "b4a": "^1.1.1"
1044 }
1045 },
1046 "node_modules/is-property": {
1047 "version": "1.0.2",
1048 "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
1049 "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
1050 "dev": true,
1051 "license": "MIT"
1052 },
1053 "node_modules/nanoassert": {
1054 "version": "2.0.0",
1055 "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz",
1056 "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==",
1057 "dev": true,
1058 "license": "ISC"
1059 },
1060 "node_modules/noise-curve-ed": {
1061 "version": "2.1.0",
1062 "resolved": "https://registry.npmjs.org/noise-curve-ed/-/noise-curve-ed-2.1.0.tgz",
1063 "integrity": "sha512-zAzJx+VwZM3w6EA1hTmDhJfvAnCeBQn/1FAeZ0LtGxCcCtlAK/uJXQVF/eDVUOaAZ286lHlx77WJ+qj9SmsRRg==",
1064 "dev": true,
1065 "license": "ISC",
1066 "dependencies": {
1067 "b4a": "^1.1.0",
1068 "nanoassert": "^2.0.0",
1069 "sodium-universal": "^5.0.0"
1070 }
1071 },
1072 "node_modules/noise-handshake": {
1073 "version": "4.2.0",
1074 "resolved": "https://registry.npmjs.org/noise-handshake/-/noise-handshake-4.2.0.tgz",
1075 "integrity": "sha512-9O/VTNX/E2/AToyMTTDU0J/4WhaXMTdqc2DHs9vf+snoZ0cenSBq0dNYTVV1snYYEkmo6QeRrYMxtqtoYnY+LA==",
1076 "dev": true,
1077 "license": "Apache-2.0",
1078 "dependencies": {
1079 "b4a": "^1.1.0",
1080 "nanoassert": "^2.0.0",
1081 "sodium-universal": "^5.0.0"
1082 }
1083 },
1084 "node_modules/paparam": {
1085 "version": "1.10.1",
1086 "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.10.1.tgz",
1087 "integrity": "sha512-viyQI64VIja0Za3njIzhoEP8ZVkgPowhZPuG0E96NwBfYJ6ZIyrrlhWGtFPkdN7eYLl2L8CTWL+wla0evm1KKQ==",
1088 "dev": true,
1089 "license": "Apache-2.0"
1090 },
1091 "node_modules/picomatch": {
1092 "version": "4.0.4",
1093 "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
1094 "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
1095 "dev": true,
1096 "license": "MIT",
1097 "engines": {
1098 "node": ">=12"
1099 },
1100 "funding": {
1101 "url": "https://github.com/sponsors/jonschlinkert"
1102 }
1103 },
1104 "node_modules/protomux": {
1105 "version": "3.10.1",
1106 "resolved": "https://registry.npmjs.org/protomux/-/protomux-3.10.1.tgz",
1107 "integrity": "sha512-jgBqx8ZyaBWea/DFG4eOu1scOaeBwcnagiRC1XFVrjeGt7oAb0Pk5udPpBUpJ4DJBRjra50jD6YcZiQQTRqaaA==",
1108 "dev": true,
1109 "license": "MIT",
1110 "dependencies": {
1111 "b4a": "^1.3.1",
1112 "compact-encoding": "^2.5.1",
1113 "queue-tick": "^1.0.0",
1114 "safety-catch": "^1.0.1",
1115 "unslab": "^1.3.0"
1116 }
1117 },
1118 "node_modules/queue-tick": {
1119 "version": "1.0.1",
1120 "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
1121 "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==",
1122 "dev": true,
1123 "license": "MIT"
1124 },
1125 "node_modules/quickbit-native": {
1126 "version": "2.4.8",
1127 "resolved": "https://registry.npmjs.org/quickbit-native/-/quickbit-native-2.4.8.tgz",
1128 "integrity": "sha512-FcCcqI+nIAWGknqhtrYT5TSD7t/N+Xd8ctM+2PrIIBuwOi5hx0SxAvuPtzLIEMfT/2h9+fhBakUe2uALOHX6yw==",
1129 "dev": true,
1130 "license": "Apache-2.0",
1131 "optional": true,
1132 "dependencies": {
1133 "require-addon": "^1.1.0"
1134 }
1135 },
1136 "node_modules/quickbit-universal": {
1137 "version": "2.2.0",
1138 "resolved": "https://registry.npmjs.org/quickbit-universal/-/quickbit-universal-2.2.0.tgz",
1139 "integrity": "sha512-w02i1R8n7+6pEKTud8DfF8zbFY9o7RtPlUc3jWbtCkDKvhbx/AvV7oNnz4/TcmsPGpSJS+fq5Ud6RH6+YPvSGg==",
1140 "dev": true,
1141 "license": "ISC",
1142 "dependencies": {
1143 "b4a": "^1.6.0",
1144 "simdle-universal": "^1.1.0"
1145 },
1146 "optionalDependencies": {
1147 "quickbit-native": "^2.2.0"
1148 }
1149 },
1150 "node_modules/random-array-iterator": {
1151 "version": "1.0.0",
1152 "resolved": "https://registry.npmjs.org/random-array-iterator/-/random-array-iterator-1.0.0.tgz",
1153 "integrity": "sha512-u7xCM93XqKEvPTP6xZp2ehttcAemKnh73oKNf1FvzuVCfpt6dILDt1Kxl1LeBjm2iNIeR49VGFhy4Iz3yOun+Q==",
1154 "dev": true,
1155 "license": "MIT"
1156 },
1157 "node_modules/ready-resource": {
1158 "version": "1.2.0",
1159 "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz",
1160 "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==",
1161 "dev": true,
1162 "license": "MIT",
1163 "dependencies": {
1164 "bare-events": "^2.2.0"
1165 }
1166 },
1167 "node_modules/refcounter": {
1168 "version": "1.0.0",
1169 "resolved": "https://registry.npmjs.org/refcounter/-/refcounter-1.0.0.tgz",
1170 "integrity": "sha512-1WosVzUy0kPUaPMEtlNDwm99UsteALIhXXR8rerELoa63WkYIXAl0hxgwPFrIYBRWZPGUyekQ04FRtPJ7dHk9w==",
1171 "dev": true,
1172 "license": "Apache-2.0"
1173 },
1174 "node_modules/require-addon": {
1175 "version": "1.2.0",
1176 "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz",
1177 "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==",
1178 "dev": true,
1179 "license": "Apache-2.0",
1180 "dependencies": {
1181 "bare-addon-resolve": "^1.3.0"
1182 },
1183 "engines": {
1184 "bare": ">=1.10.0"
1185 }
1186 },
1187 "node_modules/resolve-reject-promise": {
1188 "version": "1.1.0",
1189 "resolved": "https://registry.npmjs.org/resolve-reject-promise/-/resolve-reject-promise-1.1.0.tgz",
1190 "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==",
1191 "dev": true,
1192 "license": "MIT"
1193 },
1194 "node_modules/resource-on-exit": {
1195 "version": "1.0.0",
1196 "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz",
1197 "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==",
1198 "dev": true,
1199 "license": "Apache-2.0"
1200 },
1201 "node_modules/rocksdb-native": {
1202 "version": "3.15.0",
1203 "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.15.0.tgz",
1204 "integrity": "sha512-czYx0hOzKAy+HYmJROZm7thvJ1tHFWHNkGI3nomumXWzrJghJdrWo4MIhehGX4OTff0lnOCoB2zu6ekzIKY0rQ==",
1205 "dev": true,
1206 "license": "Apache-2.0",
1207 "dependencies": {
1208 "compact-encoding": "^2.15.0",
1209 "ready-resource": "^1.0.0",
1210 "refcounter": "^1.0.0",
1211 "require-addon": "^1.0.2",
1212 "resolve-reject-promise": "^1.1.0",
1213 "signal-promise": "^1.0.3",
1214 "streamx": "^2.16.1"
1215 },
1216 "engines": {
1217 "bare": ">=1.16.0"
1218 }
1219 },
1220 "node_modules/safety-catch": {
1221 "version": "1.0.3",
1222 "resolved": "https://registry.npmjs.org/safety-catch/-/safety-catch-1.0.3.tgz",
1223 "integrity": "sha512-Zq+J1TefpoEq/HTUabo0YXX5MNvttjWYODGohgPBO2jfko8Wqx3JYMgE823szDFVamdH5PlpByvfiWScTdSYDA==",
1224 "dev": true,
1225 "license": "MIT"
1226 },
1227 "node_modules/same-object": {
1228 "version": "1.0.2",
1229 "resolved": "https://registry.npmjs.org/same-object/-/same-object-1.0.2.tgz",
1230 "integrity": "sha512-csHWhvUsLbIOHDM/nP+KHWM+BLPsIzWkFa8HbzaI0G7BqKXgx+7FJpKTGgLXyz5amfdY2OVBcmXTqYOMEk04og==",
1231 "dev": true,
1232 "license": "MIT"
1233 },
1234 "node_modules/scope-lock": {
1235 "version": "1.2.4",
1236 "resolved": "https://registry.npmjs.org/scope-lock/-/scope-lock-1.2.4.tgz",
1237 "integrity": "sha512-BpSd8VCuCxW9ZitcdIC/vjs3gMaP9bRBL5nkHcyfX2VrS52n13/rHuBA2xJ/S/4DPuRdAO/Bk8pWd8eD/gHCIA==",
1238 "dev": true,
1239 "license": "Apache-2.0"
1240 },
1241 "node_modules/signal-promise": {
1242 "version": "1.0.3",
1243 "resolved": "https://registry.npmjs.org/signal-promise/-/signal-promise-1.0.3.tgz",
1244 "integrity": "sha512-WBgv0UnIq2C+Aeh0/n+IRpP6967eIx9WpynTUoiW3isPpfe1zu2LJzyfXdo9Tgef8yR/sGjcMvoUXD7EYdiz+g==",
1245 "dev": true,
1246 "license": "MIT"
1247 },
1248 "node_modules/simdle-native": {
1249 "version": "1.3.9",
1250 "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz",
1251 "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==",
1252 "dev": true,
1253 "license": "Apache-2.0",
1254 "optional": true,
1255 "dependencies": {
1256 "b4a": "^1.6.0",
1257 "require-addon": "^1.1.0"
1258 }
1259 },
1260 "node_modules/simdle-universal": {
1261 "version": "1.1.2",
1262 "resolved": "https://registry.npmjs.org/simdle-universal/-/simdle-universal-1.1.2.tgz",
1263 "integrity": "sha512-3n3w1bs+uwgHKQjt6arez83EywNlhZzYvNOhvAASTl/8KqNIcqr6aHyGt3JRlfuUC7iB0tomJRPlJ2cRGIpBzA==",
1264 "dev": true,
1265 "license": "ISC",
1266 "dependencies": {
1267 "b4a": "^1.6.0"
1268 },
1269 "optionalDependencies": {
1270 "simdle-native": "^1.1.1"
1271 }
1272 },
1273 "node_modules/sodium-native": {
1274 "version": "5.1.0",
1275 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.1.0.tgz",
1276 "integrity": "sha512-3RxgyWyJlhTsABPnJVpCI5CoTDANZTqqFrEPqr+kjfnRaBihpVtMUE3yTF40ukdoB1APXeoBNKF3MzZAIHg39g==",
1277 "dev": true,
1278 "license": "MIT",
1279 "dependencies": {
1280 "bare-assert": "^1.2.0",
1281 "require-addon": "^1.1.0",
1282 "which-runtime": "^1.2.1"
1283 },
1284 "engines": {
1285 "bare": ">=1.16.0"
1286 }
1287 },
1288 "node_modules/sodium-secretstream": {
1289 "version": "1.2.0",
1290 "resolved": "https://registry.npmjs.org/sodium-secretstream/-/sodium-secretstream-1.2.0.tgz",
1291 "integrity": "sha512-q/DbraNFXm1KfCiiZvapmz5UC3OlpirYFIvBK2MhGaOFSb3gRyk8OXTi17UI9SGfshQNCpsVvlopogbzZNyW6Q==",
1292 "dev": true,
1293 "license": "MIT",
1294 "dependencies": {
1295 "b4a": "^1.1.1",
1296 "sodium-universal": "^5.0.0"
1297 }
1298 },
1299 "node_modules/sodium-universal": {
1300 "version": "5.0.1",
1301 "resolved": "https://registry.npmjs.org/sodium-universal/-/sodium-universal-5.0.1.tgz",
1302 "integrity": "sha512-rv+aH+tnKB5H0MAc2UadHShLMslpJsc4wjdnHRtiSIEYpOetCgu8MS4ExQRia+GL/MK3uuCyZPeEsi+J3h+Q+Q==",
1303 "dev": true,
1304 "license": "MIT",
1305 "dependencies": {
1306 "sodium-native": "^5.0.1"
1307 },
1308 "peerDependencies": {
1309 "sodium-javascript": "~0.8.0"
1310 },
1311 "peerDependenciesMeta": {
1312 "sodium-javascript": {
1313 "optional": true
1314 }
1315 }
1316 },
1317 "node_modules/stackframe": {
1318 "version": "1.3.4",
1319 "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
1320 "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==",
1321 "dev": true,
1322 "license": "MIT"
1323 },
1324 "node_modules/streamx": {
1325 "version": "2.25.0",
1326 "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
1327 "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
1328 "dev": true,
1329 "license": "MIT",
1330 "dependencies": {
1331 "events-universal": "^1.0.0",
1332 "fast-fifo": "^1.3.2",
1333 "text-decoder": "^1.1.0"
1334 }
1335 },
1336 "node_modules/teex": {
1337 "version": "1.0.1",
1338 "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
1339 "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
1340 "dev": true,
1341 "license": "MIT",
1342 "dependencies": {
1343 "streamx": "^2.12.5"
1344 }
1345 },
1346 "node_modules/test-tmp": {
1347 "version": "1.4.0",
1348 "resolved": "https://registry.npmjs.org/test-tmp/-/test-tmp-1.4.0.tgz",
1349 "integrity": "sha512-GVggxGg+jXqP2Wbju50JVLo+9E+nIOPPyWqgr63EbOnNItIKu1cEbJpTWAJeflnyGqXOtcMI7ijHRp88GUkfDA==",
1350 "dev": true,
1351 "license": "MIT",
1352 "dependencies": {
1353 "bare-fs": "^4.0.1",
1354 "bare-os": "^3.3.0",
1355 "bare-path": "^3.0.0"
1356 }
1357 },
1358 "node_modules/text-decoder": {
1359 "version": "1.2.7",
1360 "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
1361 "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
1362 "dev": true,
1363 "license": "Apache-2.0",
1364 "dependencies": {
1365 "b4a": "^1.6.4"
1366 }
1367 },
1368 "node_modules/timeout-refresh": {
1369 "version": "2.0.1",
1370 "resolved": "https://registry.npmjs.org/timeout-refresh/-/timeout-refresh-2.0.1.tgz",
1371 "integrity": "sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==",
1372 "dev": true,
1373 "license": "MIT"
1374 },
1375 "node_modules/tmatch": {
1376 "version": "5.0.0",
1377 "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-5.0.0.tgz",
1378 "integrity": "sha512-Ib9OtBkpHn07tXP04SlN1SYRxFgTk6wSM2EBmjjxug4u5RXPRVLkdFJSS1PmrQidaSB8Lru9nRtViQBsbxzE5Q==",
1379 "dev": true,
1380 "license": "ISC",
1381 "engines": {
1382 "node": ">=8"
1383 }
1384 },
1385 "node_modules/unslab": {
1386 "version": "1.3.0",
1387 "resolved": "https://registry.npmjs.org/unslab/-/unslab-1.3.0.tgz",
1388 "integrity": "sha512-YATkfKAFj47kTzmiQrWXMyRvaVrHsW6MEALa4bm+FhiA2YG4oira+Z3DXN6LrYOYn2Y8eO94Lwl9DOHjs1FpoQ==",
1389 "dev": true,
1390 "license": "Apache-2.0",
1391 "dependencies": {
1392 "b4a": "^1.6.6"
1393 }
1394 },
1395 "node_modules/v8-to-istanbul": {
1396 "version": "9.3.0",
1397 "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
1398 "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
1399 "dev": true,
1400 "license": "ISC",
1401 "dependencies": {
1402 "@jridgewell/trace-mapping": "^0.3.12",
1403 "@types/istanbul-lib-coverage": "^2.0.1",
1404 "convert-source-map": "^2.0.0"
1405 },
1406 "engines": {
1407 "node": ">=10.12.0"
1408 }
1409 },
1410 "node_modules/which-runtime": {
1411 "version": "1.3.2",
1412 "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz",
1413 "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==",
1414 "dev": true,
1415 "license": "Apache-2.0"
1416 },
1417 "node_modules/z32": {
1418 "version": "1.1.0",
1419 "resolved": "https://registry.npmjs.org/z32/-/z32-1.1.0.tgz",
1420 "integrity": "sha512-1WUHy+VS6d0HPNspDxvLssBbeQjXMjSnpv0vH82vRAUfg847NmX3OXozp/hRP5jPhxBbrVzrgvAt+UsGNzRFQQ==",
1421 "dev": true,
1422 "license": "MIT",
1423 "dependencies": {
1424 "b4a": "^1.5.3"
1425 }
1426 }
1427 }
1428}
package.json +18 -0
……@@ -0,0 +1,18 @@
1{
2 "name": "hyper-issues",
3 "version": "0.2.0",
4 "description": "P2P issue tracking — a hyper-rooms schema for issues, comments, labels, refs",
5 "type": "module",
6 "main": "index.js",
7 "scripts": {
8 "test": "brittle test/*.test.js"
9 },
10 "dependencies": {
11 "hyper-rooms": "file:../hyper-rooms"
12 },
13 "devDependencies": {
14 "autobase-test-helpers": "^2.0.0",
15 "brittle": "^3.0.0",
16 "corestore": "^7.0.0"
17 }
18}
test/basic.test.js +316 -0
……@@ -0,0 +1,316 @@
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 { HyperIssues } from '../index.js'
8
9function tmpStore () {
10 const dir = mkdtempSync(join(tmpdir(), 'hyper-issues-test-'))
11 return new Corestore(dir)
12}
13
14async function makeTracker (t, namespace = 'test') {
15 const store = tmpStore()
16 const tracker = new HyperIssues(store, { namespace })
17 await tracker.ready()
18 t.teardown(async () => {
19 try { await tracker.close() } catch {}
20 try { await store.close() } catch {}
21 })
22 return tracker
23}
24
25test('create and retrieve issue', async (t) => {
26 const tracker = await makeTracker(t, 'test-1')
27
28 const id = await tracker.create({
29 title: 'First bug',
30 body: 'Something is broken',
31 author: 'user-1'
32 })
33
34 t.is(id, 1)
35
36 const issue = await tracker.get(id)
37 t.is(issue.title, 'First bug')
38 t.is(issue.body, 'Something is broken')
39 t.is(issue.author, 'user-1')
40 t.is(issue.status, 'open')
41})
42
43test('update issue status', async (t) => {
44 const tracker = await makeTracker(t, 'test-2')
45
46 const id = await tracker.create({ title: 'Bug', author: 'user-1' })
47 await tracker.update(id, { status: 'closed' }, 'user-1')
48
49 const issue = await tracker.get(id)
50 t.is(issue.status, 'closed')
51})
52
53test('list issues with filters', async (t) => {
54 const tracker = await makeTracker(t, 'test-3')
55
56 await tracker.create({ title: 'Open bug', author: 'user-1' })
57 await tracker.create({ title: 'Another open', author: 'user-2' })
58 const closedId = await tracker.create({ title: 'Closed one', author: 'user-1' })
59 await tracker.update(closedId, { status: 'closed' }, 'user-1')
60
61 const open = await tracker.list({ status: 'open' })
62 t.is(open.length, 2)
63
64 const closed = await tracker.list({ status: 'closed' })
65 t.is(closed.length, 1)
66
67 const all = await tracker.list()
68 t.is(all.length, 3)
69
70 const byAuthor = await tracker.list({ author: 'user-1' })
71 t.is(byAuthor.length, 2)
72
73 const searched = await tracker.list({ search: 'another' })
74 t.is(searched.length, 1)
75})
76
77test('comments', async (t) => {
78 const tracker = await makeTracker(t, 'test-4')
79
80 const id = await tracker.create({ title: 'Bug', author: 'user-1' })
81
82 const commentId = await tracker.comment(id, {
83 body: 'I can reproduce this',
84 author: 'user-2'
85 })
86
87 t.ok(commentId)
88
89 const comments = await tracker.getComments(id)
90 t.is(comments.length, 1)
91 t.is(comments[0].body, 'I can reproduce this')
92 t.is(comments[0].author, 'user-2')
93})
94
95test('edit and delete comments', async (t) => {
96 const tracker = await makeTracker(t, 'test-4b')
97
98 const id = await tracker.create({ title: 'Bug', author: 'user-1' })
99 const cId = await tracker.comment(id, { body: 'first', author: 'user-2' })
100
101 await tracker.editComment(id, cId, 'edited', 'user-2')
102 let comments = await tracker.getComments(id)
103 t.is(comments[0].body, 'edited')
104 t.ok(comments[0].editedAt)
105 t.is(comments[0].editedBy, 'user-2')
106
107 await tracker.deleteComment(id, cId, 'user-2')
108 comments = await tracker.getComments(id)
109 t.is(comments.length, 0)
110})
111
112test('labels', async (t) => {
113 const tracker = await makeTracker(t, 'test-5')
114
115 await tracker.createLabel('bug', '#ff0000', 'Something broken', 'Bug')
116 await tracker.createLabel('feature', '#00ff00', 'New feature', 'Feature')
117
118 const labels = await tracker.listLabels()
119 t.is(labels.length, 2)
120
121 const bug = await tracker.getLabel('bug')
122 t.is(bug.color, '#ff0000')
123 t.is(bug.displayName, 'Bug')
124
125 await tracker.updateLabel('bug', { color: '#aa0000' })
126 const updated = await tracker.getLabel('bug')
127 t.is(updated.color, '#aa0000')
128
129 await tracker.deleteLabel('feature')
130 const after = await tracker.listLabels()
131 t.is(after.length, 1)
132})
133
134test('issue counts', async (t) => {
135 const tracker = await makeTracker(t, 'test-6')
136
137 await tracker.create({ title: 'one', author: 'user-1' })
138 await tracker.create({ title: 'two', author: 'user-1' })
139 const id3 = await tracker.create({ title: 'three', author: 'user-1' })
140 await tracker.update(id3, { status: 'closed' }, 'user-1')
141
142 const counts = await tracker.counts()
143 t.is(counts.open, 2)
144 t.is(counts.closed, 1)
145 t.is(counts.total, 3)
146})
147
148test('auto-incrementing IDs', async (t) => {
149 const tracker = await makeTracker(t, 'test-7')
150
151 const id1 = await tracker.create({ title: 'a', author: 'u' })
152 const id2 = await tracker.create({ title: 'b', author: 'u' })
153 const id3 = await tracker.create({ title: 'c', author: 'u' })
154
155 t.is(id1, 1)
156 t.is(id2, 2)
157 t.is(id3, 3)
158})
159
160test('refs (optional git feature)', async (t) => {
161 const tracker = await makeTracker(t, 'test-8')
162
163 await tracker.updateRef('refs/heads/main', 'abc123', 'user-1')
164 await tracker.updateRef('refs/heads/dev', 'def456', 'user-1')
165
166 const main = await tracker.getRef('refs/heads/main')
167 t.is(main.hash, 'abc123')
168
169 const refs = await tracker.listRefs()
170 t.is(refs.length, 2)
171
172 await tracker.deleteRef('refs/heads/dev', 'user-1')
173 const after = await tracker.listRefs()
174 t.is(after.length, 1)
175})
176
177test('meta key-value storage', async (t) => {
178 const tracker = await makeTracker(t, 'test-9')
179
180 await tracker.setMeta('description', 'My project')
181 await tracker.setMeta('homepage', 'https://example.com')
182
183 t.is(await tracker.getMeta('description'), 'My project')
184 t.is(await tracker.getMeta('homepage'), 'https://example.com')
185 t.is(await tracker.getMeta('missing'), null)
186})
187
188test('key properties', async (t) => {
189 const tracker = await makeTracker(t, 'test-10')
190
191 t.ok(tracker.key, 'has key')
192 t.is(typeof tracker.keyHex, 'string')
193 t.is(tracker.keyHex.length, 64, 'keyHex is 32 bytes hex')
194 t.ok(tracker.localKey, 'has localKey')
195 t.is(tracker.writable, true, 'first writer is writable')
196})
197
198test('add labels to an issue', async (t) => {
199 const tracker = await makeTracker(t, 'test-11')
200
201 await tracker.createLabel('bug', '#ff0000')
202 await tracker.createLabel('urgent', '#ffaa00')
203
204 const id = await tracker.create({ title: 'Bug', author: 'user-1' })
205 await tracker.addLabels(id, ['bug', 'urgent'], 'user-1')
206
207 const issue = await tracker.get(id)
208 t.alike(issue.labels.sort(), ['bug', 'urgent'])
209
210 await tracker.removeLabels(id, ['urgent'], 'user-1')
211 const after = await tracker.get(id)
212 t.alike(after.labels, ['bug'])
213})
214
215test('initializeDefaultLabels', async (t) => {
216 const tracker = await makeTracker(t, 'test-12')
217
218 const labels = await tracker.initializeDefaultLabels()
219 t.ok(labels.length > 0, 'creates default labels')
220 t.ok(labels.find(l => l.name === 'bug'))
221 t.ok(labels.find(l => l.name === 'enhancement'))
222
223 // Calling again should not duplicate
224 await tracker.initializeDefaultLabels()
225 const second = await tracker.listLabels()
226 t.is(second.length, labels.length)
227})
228
229// ── Multi-writer replication ──────────────────────────────────
230
231test('two trackers replicate via autobase', async (t) => {
232 const storeA = tmpStore()
233 const storeB = tmpStore()
234
235 const trackerA = new HyperIssues(storeA, { namespace: 'shared' })
236 await trackerA.ready()
237
238 // A creates an issue and a label
239 await trackerA.createLabel('bug', '#ff0000')
240 await trackerA.create({ title: 'shared bug', author: 'alice' })
241 await trackerA.base.update()
242
243 const trackerB = new HyperIssues(storeB, {
244 namespace: 'shared',
245 bootstrapKey: trackerA.keyHex
246 })
247 await trackerB.ready()
248
249 await replicateAndSync([trackerA.base, trackerB.base])
250
251 // B should see A's data
252 const issue = await trackerB.get(1)
253 t.ok(issue, 'B received the issue')
254 t.is(issue.title, 'shared bug')
255
256 const labels = await trackerB.listLabels()
257 t.is(labels.length, 1)
258 t.is(labels[0].name, 'bug')
259
260 t.teardown(async () => {
261 try { await trackerA.close() } catch {}
262 try { await trackerB.close() } catch {}
263 try { await storeA.close() } catch {}
264 try { await storeB.close() } catch {}
265 })
266})
267
268test('two writers can both append after addWriter', async (t) => {
269 const storeA = tmpStore()
270 const storeB = tmpStore()
271
272 const trackerA = new HyperIssues(storeA, { namespace: 'shared2' })
273 await trackerA.ready()
274
275 const trackerB = new HyperIssues(storeB, {
276 namespace: 'shared2',
277 bootstrapKey: trackerA.keyHex
278 })
279 await trackerB.ready()
280
281 // Initial sync so B knows the room structure
282 await replicateAndSync([trackerA.base, trackerB.base])
283
284 // A authorizes B as a writer
285 await trackerA.addWriter('bob', { writerKey: trackerB.localKeyHex, role: 'writer' })
286 await replicateAndSync([trackerA.base, trackerB.base])
287
288 t.ok(trackerB.writable, 'B is now writable')
289
290 // Alice creates first, sync, then Bob creates.
291 // (Concurrent create() from two writers picks the same auto-increment ID
292 // because each writer reads its local counter independently. This is a
293 // known limitation of auto-increment in multiwriter — to avoid the
294 // collision, sync between creates so each peer sees the latest counter.
295 // For collision-free concurrent IDs, use UUID-style IDs in the schema.)
296 await trackerA.create({ title: 'from alice', author: 'alice' })
297 await replicateAndSync([trackerA.base, trackerB.base])
298 await trackerB.create({ title: 'from bob', author: 'bob' })
299
300 await replicateAndSync([trackerA.base, trackerB.base])
301
302 // Both should see both issues
303 const allA = await trackerA.list({ sort: 'oldest' })
304 const allB = await trackerB.list({ sort: 'oldest' })
305
306 t.is(allA.length, 2, 'A sees both')
307 t.is(allB.length, 2, 'B sees both')
308 t.alike(allA.map(i => i.title).sort(), ['from alice', 'from bob'])
309
310 t.teardown(async () => {
311 try { await trackerA.close() } catch {}
312 try { await trackerB.close() } catch {}
313 try { await storeA.close() } catch {}
314 try { await storeB.close() } catch {}
315 })
316})