@dtudury/streamo 2.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,18 +11,35 @@ Streamo is a peer-to-peer sync library built around a simple promise: **no serve
11
11
  - **Every write is provably yours** — commits are signed with your keypair and append-only. History is permanent and can't be forged; peers reject unsigned or mis-signed data.
12
12
  - **Content-addressed** — data is identified by what it is, not where it lives. The same value always lands at the same address; deduplication and diffing are structural.
13
13
 
14
- ## install
14
+ ## three ways to use streamo
15
15
 
16
- ```bash
17
- npm install @dtudury/streamo
18
- ```
16
+ There are basically three audiences. Pick the one that's you:
17
+
18
+ **1. As a library** — `npm install @dtudury/streamo` and import the pieces
19
+ you want. See the *javascript api* section below for what's exported.
19
20
 
20
- Or run the CLI directly:
21
+ **2. As a CLI** — `npx @dtudury/streamo --help` runs the streamo CLI
22
+ without cloning anything. You bring credentials, point at files or peers,
23
+ and get a personal streamo node. See the *cli* section.
24
+
25
+ **3. As a reference / contributor** — clone this repo, then:
21
26
 
22
27
  ```bash
23
- npx @dtudury/streamo --help
28
+ npm install
29
+ npm run dev # starts the all-in-one demo on port 8080
24
30
  ```
25
31
 
32
+ `npm run dev` runs the chat-room server (`public/apps/chat/server.js`) with
33
+ the checked-in dev credentials in `.env.dev`. That one server hosts the
34
+ homepage, chat app, **and** the repo explorer at `localhost:8080`. Modify
35
+ any file, refresh, see the change.
36
+
37
+ For production deployment, your real `.env.prod` lives only on the
38
+ production host, and `npm run prod` boots the same server against that
39
+ env.
40
+
41
+ `npm test` runs the test suite.
42
+
26
43
  ## cli
27
44
 
28
45
  ```bash
@@ -177,21 +194,29 @@ For hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn
177
194
  | `s3Sync` | replicate chunks to S3-compatible object storage |
178
195
  | `stateFileSync` | write repo state as JSON on every change |
179
196
 
180
- ## chat example
197
+ ## what `npm run dev` actually starts
181
198
 
182
- ```bash
183
- # start the server its public key becomes the room key
184
- STREAMO_NAME=my-chat STREAMO_USERNAME=relay STREAMO_PASSWORD=secret \
185
- node public/apps/chat/server.js
199
+ The chat-room server. It's the all-in-one demo: the homepage, chat app,
200
+ and repo explorer are all served by the same process on port 8080. The
201
+ "server" is just another streamo node — it holds the room's member list
202
+ in its own repo and auto-accepts anyone who announces to it. Its public
203
+ key is the room address. No special authority, no hidden state.
204
+
205
+ Useful URLs once it's running:
186
206
 
187
- # join from the browser
188
- open http://localhost:8080/apps/chat/
207
+ - `http://localhost:8080/` homepage with app cards
208
+ - `http://localhost:8080/apps/chat/` — chat
209
+ - `http://localhost:8080/apps/explorer/` — repo explorer (leave it open in
210
+ another tab to watch commits roll in as you chat)
189
211
 
190
- # join from the terminal
212
+ To join chat from a terminal instead of the browser:
213
+
214
+ ```bash
191
215
  node public/streamo/chat-cli.js alice secret localhost 8080
192
216
  ```
193
217
 
194
- Each participant owns their own message stream. The server is just another streamo node — it holds the member list in its own repo and auto-accepts anyone who announces to it. Its public key is the room address. No special authority, no hidden state.
218
+ Each participant owns their own message stream. Same data structure,
219
+ different transport.
195
220
 
196
221
  ## tests
197
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtudury/streamo",
3
- "version": "2.0.0",
3
+ "version": "4.0.0",
4
4
  "description": "peer-to-peer sync where your data and identity belong to you, not the server",
5
5
  "keywords": ["p2p", "peer-to-peer", "sync", "reactive", "content-addressed", "websocket", "signed", "append-only", "offline-first", "cryptographic", "identity"],
6
6
  "repository": "git@github.com:dtudury/streamo.git",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "scripts": {
38
38
  "test": "node --test",
39
- "serve": "node bin/streamo.js --env-file .env.dev --web 3000 --interactive",
40
- "chat": "node public/apps/chat/server.js --env-file .env.dev"
39
+ "dev": "node public/apps/chat/server.js --env-file .env.dev",
40
+ "prod": "node public/apps/chat/server.js --env-file .env.prod"
41
41
  }
42
42
  }
@@ -43,16 +43,31 @@ joinBtn.onclick = async () => {
43
43
  const myKey = bytesToHex(publicKey)
44
44
  const registry = new RepoRegistry()
45
45
 
46
+ // Track who we've already announced ourselves back to, so we don't
47
+ // ping-pong forever. Without this set, every peer-back ricochets into
48
+ // another peer-back and so on.
49
+ const announcedTo = new Set()
46
50
  const session = await registrySync(registry, location.hostname, Number(location.port) || 80, {
47
51
  filter: k => k === rootKey,
48
52
  follow: (keyHex, repo, subscribe) => {
49
53
  for (const memberKey of repo.get('members') ?? []) subscribe(memberKey)
50
54
  },
51
- onAnnounce: key => session.subscribe(key)
55
+ // When a peer announces, subscribe to them AND announce ourselves
56
+ // back so they learn we exist — this makes peer discovery work
57
+ // through pure real-time fan-out, no server-side member tracking
58
+ // required. Late-joiner sees us, we see late-joiner.
59
+ onAnnounce: key => {
60
+ session.subscribe(key)
61
+ if (!announcedTo.has(key)) {
62
+ announcedTo.add(key)
63
+ session.announce(myKey, rootKey)
64
+ }
65
+ }
52
66
  })
53
67
 
54
68
  const myRepo = await registry.open(myKey)
55
69
  myRepo.attachSigner(signer, 'chat')
70
+ myRepo.defaultMessage = `joined as ${username} (web)`
56
71
 
57
72
  session.interest(rootKey)
58
73
  session.announce(myKey, rootKey)
@@ -112,6 +127,8 @@ joinBtn.onclick = async () => {
112
127
  if (!text) return
113
128
  inputEl.value = ''
114
129
  const messages = myRepo.get('messages') ?? []
130
+ const preview = text.length > 50 ? text.slice(0, 50).trim() + '…' : text
131
+ myRepo.defaultMessage = `"${preview}" (web)`
115
132
  myRepo.set({ name: username, messages: [...messages, { text, at: Date.now() }] })
116
133
  }
117
134
 
@@ -0,0 +1,303 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>streamo explorer</title>
7
+ <link rel="stylesheet" href="/apps/styles/proto.css">
8
+ <style>
9
+ body { max-width: 60rem; margin: 0 auto; padding: 2rem 1.25rem; }
10
+
11
+ .header { display: flex; align-items: baseline; gap: 0.75rem; margin-bottom: 0.25rem; }
12
+ .wordmark { font-size: 1.6rem; letter-spacing: -0.02em; }
13
+ .crumbs { font-size: 0.85rem; color: var(--ink-dim); }
14
+ .back { cursor: pointer; color: var(--ink-dim); font-size: 0.85rem; display: inline-block; margin-bottom: 1rem; }
15
+ .back:hover { color: var(--ink); }
16
+
17
+ h2 { font-size: 1.05rem; font-weight: 600; margin: 1.25rem 0 0.5rem; }
18
+ h2 .dim { font-weight: 400; font-size: 0.9rem; }
19
+
20
+ .row {
21
+ display: grid;
22
+ grid-template-columns: 1fr 12rem 14rem;
23
+ gap: 0.75rem;
24
+ align-items: baseline;
25
+ padding: 0.55rem 0.75rem;
26
+ border: 1.5px solid transparent;
27
+ border-radius: var(--radius);
28
+ cursor: pointer;
29
+ }
30
+ .row:hover { border-color: var(--ink); background: rgba(254, 240, 138, 0.4); }
31
+ .row + .row { border-top-color: var(--rule); }
32
+ .row:hover + .row { border-top-color: transparent; }
33
+
34
+ /* signed-commit + unsigned-commit + commit + signature rows share the same
35
+ column template so the page doesn't visually jitter as you scan a mixed
36
+ list. cols: kind | message | date | addr. */
37
+ .row.signed-commit, .row.unsigned-commit,
38
+ .row.commit, .row.signature { grid-template-columns: 6rem 1fr 14rem 6rem; }
39
+
40
+ .row .mono { font-size: 0.85rem; }
41
+ .row .when { font-size: 0.78rem; color: var(--ink-dim); }
42
+ .row .msg { font-size: 0.85rem; }
43
+ .row .kind {
44
+ font-size: 0.7rem;
45
+ text-transform: uppercase;
46
+ letter-spacing: 0.08em;
47
+ color: var(--ink-dim);
48
+ border: 1px solid var(--rule);
49
+ border-radius: 999px;
50
+ padding: 0.05rem 0.5rem;
51
+ text-align: center;
52
+ align-self: center;
53
+ }
54
+ .row.commit .kind { color: var(--accent); border-color: var(--accent); }
55
+ .row.signature .kind { color: var(--warn); border-color: var(--warn); }
56
+ .row.signed-commit .kind { color: #16a34a; border-color: #16a34a; }
57
+ .row.unsigned-commit .kind { color: var(--warn); border-color: var(--warn); }
58
+
59
+ /* HEAD card — the most-recent signed commit, prominent and self-orienting. */
60
+ .row.signed-commit.head-card {
61
+ border: 1.5px solid #16a34a;
62
+ background: rgba(22, 163, 74, 0.05);
63
+ padding: 0.85rem;
64
+ }
65
+ .row.signed-commit.head-card .msg { font-size: 1rem; font-weight: 500; }
66
+
67
+ /* Commit selector: a real dropdown widget. Summary = currently-selected
68
+ commit (HEAD by default), styled as the green head-card. Body =
69
+ full list of signed commits, with the selected one marked. */
70
+ details.commit-selector { margin: 0.5rem 0 1rem; }
71
+ details.commit-selector > summary {
72
+ cursor: pointer;
73
+ list-style: none;
74
+ padding: 0;
75
+ }
76
+ details.commit-selector > summary::-webkit-details-marker { display: none; }
77
+ details.commit-selector > summary::marker { display: none; }
78
+ details.commit-selector > summary::after {
79
+ content: '▾';
80
+ float: right;
81
+ margin: 0.5rem 0.85rem;
82
+ color: #16a34a;
83
+ font-size: 0.85rem;
84
+ }
85
+ details.commit-selector[open] > summary::after { content: '▴'; }
86
+ details.commit-selector .dropdown-body {
87
+ margin-top: 0.25rem;
88
+ border: 1px solid var(--rule);
89
+ border-radius: var(--radius);
90
+ padding: 0.25rem;
91
+ }
92
+ details.commit-selector .dropdown-body .row { padding: 0.45rem 0.6rem; }
93
+ details.commit-selector .dropdown-body .row.selected {
94
+ background: rgba(22, 163, 74, 0.07);
95
+ }
96
+ details.commit-selector .dropdown-body .row.selected .kind::after {
97
+ content: ' ●';
98
+ color: #16a34a;
99
+ }
100
+
101
+ /* Tucked-away secondary "storage chunks" list at the bottom of the
102
+ repo view — a click away when you want to see the bytes underneath. */
103
+ details.other-storage {
104
+ margin: 1.25rem 0 0.5rem;
105
+ border-top: 1px solid var(--rule);
106
+ padding-top: 0.5rem;
107
+ }
108
+ details.other-storage > summary {
109
+ cursor: pointer;
110
+ font-size: 0.85rem;
111
+ color: var(--ink-dim);
112
+ padding: 0.35rem 0.25rem;
113
+ }
114
+ details.other-storage[open] > summary { color: var(--ink); }
115
+
116
+ /* Polished signed-commit detail view (AtView SIGNATURE branch). */
117
+ .signed-commit-banner {
118
+ display: flex; align-items: center; gap: 0.5rem;
119
+ padding: 0.65rem 0.85rem; margin: 0.5rem 0 1rem;
120
+ border: 1.5px solid #16a34a; border-radius: var(--radius);
121
+ background: rgba(22, 163, 74, 0.06);
122
+ }
123
+ .signed-commit-banner .signed-label {
124
+ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em;
125
+ font-weight: 600; color: #16a34a;
126
+ }
127
+ .commit-card {
128
+ padding: 0.6rem 0.85rem; margin: 0.4rem 0;
129
+ border: 1px solid var(--rule); border-radius: var(--radius);
130
+ }
131
+ .commit-card .commit-msg { font-size: 0.95rem; margin-bottom: 0.25rem; }
132
+ .commit-card .commit-meta { font-size: 0.8rem; }
133
+
134
+ .verify-badge { font-weight: 700; padding-left: 0.35em; font-size: 0.95em; }
135
+ .verify-badge.valid { color: #16a34a; }
136
+ .verify-badge.invalid { color: #dc2626; }
137
+ .verify-badge.pending { color: var(--ink-dim); font-weight: 400; }
138
+ .verify-badge.error { color: #ca8a04; }
139
+
140
+ .empty { color: var(--ink-dim); padding: 0.5rem 0.75rem; font-size: 0.9rem; }
141
+
142
+ /* key/value table for the at-view */
143
+ .kv { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin: 0.75rem 0; }
144
+ .kv td { padding: 0.4rem 0.6rem; vertical-align: top; }
145
+ .kv tr + tr td { border-top: 1px dashed var(--rule); }
146
+ .kv td:first-child {
147
+ color: var(--ink-dim);
148
+ width: 8rem;
149
+ font-size: 0.78rem;
150
+ text-transform: uppercase;
151
+ letter-spacing: 0.06em;
152
+ }
153
+
154
+ /* clickable variant — whole row is the click target */
155
+ .kv.clickable tr { cursor: pointer; }
156
+ .kv.clickable tr:hover td { background: rgba(254, 240, 138, 0.4); }
157
+ .kv.clickable td:last-child { color: var(--accent); text-align: right; }
158
+
159
+ .addr-link {
160
+ font-family: monospace;
161
+ font-size: 0.85rem;
162
+ color: var(--accent);
163
+ cursor: pointer;
164
+ text-decoration: underline dotted;
165
+ }
166
+ .addr-link:hover { background: var(--flash); text-decoration-style: solid; }
167
+
168
+ .paths { list-style: none; padding: 0; }
169
+ .paths li { padding: 0.2rem 0.5rem; font-size: 0.85rem; }
170
+ .paths li + li { border-top: 1px dashed var(--rule); }
171
+
172
+ h3 { font-size: 0.9rem; font-weight: 600; margin: 1.25rem 0 0.5rem; }
173
+ h3 .dim { font-weight: 400; font-size: 0.85rem; }
174
+
175
+ .explainer {
176
+ font-size: 0.85rem;
177
+ line-height: 1.55;
178
+ color: var(--ink-dim);
179
+ border-left: 2px solid var(--rule);
180
+ padding: 0.4rem 0 0.4rem 0.85rem;
181
+ margin: 0.6rem 0 0.9rem;
182
+ }
183
+ .explainer strong { color: var(--ink); }
184
+
185
+ .conn { font-size: 0.75rem; color: var(--ink-dim); margin-bottom: 1.5rem; }
186
+ .conn.ok { color: #16a34a; }
187
+ .conn.err { color: #dc2626; }
188
+
189
+ .keyfull { font-size: 0.78rem; color: var(--ink-dim); word-break: break-all; }
190
+ .keyfull .mono { font-family: monospace; }
191
+ .repo-link {
192
+ font-family: monospace;
193
+ color: var(--accent);
194
+ cursor: pointer;
195
+ text-decoration: underline dotted;
196
+ }
197
+ .repo-link:hover { background: var(--flash); text-decoration-style: solid; }
198
+
199
+ /* Byte stream — zoomed strip in a horizontally-scrollable container,
200
+ click-drag-to-pan inside for "look around" navigation. */
201
+ .byte-strip-container {
202
+ width: 100%;
203
+ overflow-x: auto;
204
+ background: #faf9f4;
205
+ border: 1.5px solid var(--rule);
206
+ border-radius: var(--radius);
207
+ margin: 0.4rem 0 1rem;
208
+ cursor: grab;
209
+ }
210
+ .byte-strip-container.dragging { cursor: grabbing; user-select: none; }
211
+ .byte-strip-container.dragging .chunk { cursor: grabbing; }
212
+ .byte-strip { display: block; }
213
+
214
+ .byte-map {
215
+ display: block;
216
+ }
217
+ .byte-map .chunk {
218
+ cursor: pointer;
219
+ stroke: rgba(0, 0, 0, 0.15);
220
+ stroke-width: 0.4;
221
+ transition: stroke-width 0.08s, fill-opacity 0.08s;
222
+ }
223
+ .byte-map .chunk:hover { stroke: var(--ink); stroke-width: 1.5; }
224
+ .byte-map .chunk.current { stroke: var(--ink); stroke-width: 2; }
225
+ .byte-map .chunk.hovered { fill-opacity: 0.55; }
226
+
227
+ /* codec category palette — used in both the legend and the SVG fills */
228
+ .cat-commit { fill: #f59e0b; background: #f59e0b; }
229
+ .cat-sig { fill: #ef4444; background: #ef4444; }
230
+ .cat-composite { fill: #3b82f6; background: #3b82f6; }
231
+ .cat-duple { fill: #a855f7; background: #a855f7; }
232
+ .cat-string { fill: #10b981; background: #10b981; }
233
+ .cat-bytes { fill: #84cc16; background: #84cc16; }
234
+ .cat-num { fill: #64748b; background: #64748b; }
235
+ .cat-var { fill: #fbbf24; background: #fbbf24; }
236
+ .cat-other { fill: #cbd5e1; background: #cbd5e1; }
237
+
238
+ .byte-map-legend {
239
+ display: flex;
240
+ gap: 0.5rem;
241
+ flex-wrap: wrap;
242
+ font-size: 0.7rem;
243
+ color: var(--ink-dim);
244
+ margin-top: 0.5rem;
245
+ }
246
+ .byte-map-legend span {
247
+ display: inline-flex;
248
+ align-items: center;
249
+ gap: 0.3rem;
250
+ padding: 0.05rem 0.45rem 0.05rem 0.3rem;
251
+ border-radius: 999px;
252
+ color: #fff;
253
+ font-weight: 500;
254
+ letter-spacing: 0.02em;
255
+ }
256
+
257
+ /* Tab strip — hand-drawn underline aesthetic to match proto.css */
258
+ .tabs {
259
+ display: flex;
260
+ gap: 1.25rem;
261
+ border-bottom: 1.5px solid var(--rule);
262
+ margin: 1.25rem 0 1rem;
263
+ }
264
+ .tab {
265
+ padding: 0.45rem 0.1rem;
266
+ margin-bottom: -1.5px;
267
+ border-bottom: 2px solid transparent;
268
+ cursor: pointer;
269
+ font-size: 0.85rem;
270
+ color: var(--ink-dim);
271
+ letter-spacing: 0.04em;
272
+ text-transform: lowercase;
273
+ }
274
+ .tab:hover { color: var(--ink); }
275
+ .tab.active {
276
+ color: var(--ink);
277
+ border-bottom-color: var(--ink);
278
+ font-weight: 600;
279
+ }
280
+
281
+ pre.value {
282
+ font-family: monospace;
283
+ font-size: 0.8rem;
284
+ background: var(--rule);
285
+ border-radius: var(--radius);
286
+ padding: 1rem;
287
+ overflow-x: auto;
288
+ white-space: pre-wrap;
289
+ word-break: break-word;
290
+ }
291
+ </style>
292
+ </head>
293
+ <body>
294
+ <div class="header">
295
+ <div class="wordmark">streamo</div>
296
+ <div class="crumbs">explorer</div>
297
+ </div>
298
+ <div id="conn" class="conn">connecting…</div>
299
+ <div id="app"></div>
300
+
301
+ <script type="module" src="./main.js"></script>
302
+ </body>
303
+ </html>