@dtudury/streamo 3.0.0 → 4.0.1

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,31 +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
- ## the all-in-one demo
197
+ ## what `npm run dev` actually starts
181
198
 
182
- The chat server is also the website server. Run it once and you get the
183
- homepage, chat app, **and** the repo explorer all on the same origin:
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.
184
204
 
185
- ```bash
186
- # start the all-in-one demo server
187
- STREAMO_NAME=my-chat STREAMO_USERNAME=relay STREAMO_PASSWORD=secret \
188
- node public/apps/chat/server.js
205
+ Useful URLs once it's running:
189
206
 
190
- # homepage with app cards
191
- open http://localhost:8080/
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)
192
211
 
193
- # chat
194
- open http://localhost:8080/apps/chat/
212
+ To join chat from a terminal instead of the browser:
195
213
 
196
- # repo explorer — leave it open in another tab to watch commits roll in
197
- # as you chat
198
- open http://localhost:8080/apps/explorer/
199
-
200
- # join chat from the terminal
214
+ ```bash
201
215
  node public/streamo/chat-cli.js alice secret localhost 8080
202
216
  ```
203
217
 
204
- 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.
205
220
 
206
221
  ## tests
207
222
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtudury/streamo",
3
- "version": "3.0.0",
3
+ "version": "4.0.1",
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
  }
@@ -13,8 +13,10 @@ function fmt (ts) {
13
13
  }
14
14
 
15
15
  function Msg ({ name, text, at, mine }) {
16
+ // +at coerces both Date and number to ms — stable key across old (number)
17
+ // and new (Date) message records as we transition.
16
18
  return h`
17
- <div class=${['msg', mine ? 'mine' : 'theirs']} data-key=${at}>
19
+ <div class=${['msg', mine ? 'mine' : 'theirs']} data-key=${+at}>
18
20
  ${!mine ? h`<div class="sender">${name}</div>` : null}
19
21
  <div class="text">${text}</div>
20
22
  <div class="time">${fmt(at)}</div>
@@ -43,16 +45,31 @@ joinBtn.onclick = async () => {
43
45
  const myKey = bytesToHex(publicKey)
44
46
  const registry = new RepoRegistry()
45
47
 
48
+ // Track who we've already announced ourselves back to, so we don't
49
+ // ping-pong forever. Without this set, every peer-back ricochets into
50
+ // another peer-back and so on.
51
+ const announcedTo = new Set()
46
52
  const session = await registrySync(registry, location.hostname, Number(location.port) || 80, {
47
53
  filter: k => k === rootKey,
48
54
  follow: (keyHex, repo, subscribe) => {
49
55
  for (const memberKey of repo.get('members') ?? []) subscribe(memberKey)
50
56
  },
51
- onAnnounce: key => session.subscribe(key)
57
+ // When a peer announces, subscribe to them AND announce ourselves
58
+ // back so they learn we exist — this makes peer discovery work
59
+ // through pure real-time fan-out, no server-side member tracking
60
+ // required. Late-joiner sees us, we see late-joiner.
61
+ onAnnounce: key => {
62
+ session.subscribe(key)
63
+ if (!announcedTo.has(key)) {
64
+ announcedTo.add(key)
65
+ session.announce(myKey, rootKey)
66
+ }
67
+ }
52
68
  })
53
69
 
54
70
  const myRepo = await registry.open(myKey)
55
71
  myRepo.attachSigner(signer, 'chat')
72
+ myRepo.defaultMessage = `joined as ${username} (web)`
56
73
 
57
74
  session.interest(rootKey)
58
75
  session.announce(myKey, rootKey)
@@ -112,7 +129,9 @@ joinBtn.onclick = async () => {
112
129
  if (!text) return
113
130
  inputEl.value = ''
114
131
  const messages = myRepo.get('messages') ?? []
115
- myRepo.set({ name: username, messages: [...messages, { text, at: Date.now() }] })
132
+ const preview = text.length > 50 ? text.slice(0, 50).trim() + '…' : text
133
+ myRepo.defaultMessage = `"${preview}" (web)`
134
+ myRepo.set({ name: username, messages: [...messages, { text, at: new Date() }] })
116
135
  }
117
136
 
118
137
  sendBtn.onclick = sendMessage
@@ -31,9 +31,11 @@
31
31
  .row + .row { border-top-color: var(--rule); }
32
32
  .row:hover + .row { border-top-color: transparent; }
33
33
 
34
- /* signature rows show 4 columns: kind, range, hex, addr */
35
- .row.signature { grid-template-columns: 4rem 1fr 1fr 6rem; }
36
- .row.commit { grid-template-columns: 4rem 1fr 12rem 6rem; }
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; }
37
39
 
38
40
  .row .mono { font-size: 0.85rem; }
39
41
  .row .when { font-size: 0.78rem; color: var(--ink-dim); }
@@ -49,8 +51,115 @@
49
51
  text-align: center;
50
52
  align-self: center;
51
53
  }
52
- .row.commit .kind { color: var(--accent); border-color: var(--accent); }
53
- .row.signature .kind { color: var(--warn); border-color: var(--warn); }
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.signed-commit.unsigned .kind { color: var(--ink-dim); border-color: var(--ink-dim); }
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
+ /* Detached card — same layout as the head-card but neutral styling.
68
+ Shown as the selector summary when the current address isn't a sig
69
+ (you've drilled into raw memory). The dropdown body is still the
70
+ way back — pick a real commit and you re-attach. */
71
+ .row.signed-commit.detached-card {
72
+ border: 1.5px dashed var(--rule);
73
+ background: transparent;
74
+ padding: 0.85rem;
75
+ cursor: pointer;
76
+ }
77
+ .row.signed-commit.detached-card .kind {
78
+ color: var(--ink-dim);
79
+ border-color: var(--ink-dim);
80
+ }
81
+ .row.signed-commit.detached-card .msg { font-size: 0.95rem; }
82
+
83
+ /* Commit selector: a real dropdown widget. Summary = currently-selected
84
+ commit (HEAD by default), styled as the green head-card. Body =
85
+ full list of signed commits, with the selected one marked. */
86
+ details.commit-selector { margin: 0.5rem 0 1rem; }
87
+ details.commit-selector > summary {
88
+ cursor: pointer;
89
+ list-style: none;
90
+ padding: 0;
91
+ }
92
+ details.commit-selector > summary::-webkit-details-marker { display: none; }
93
+ details.commit-selector > summary::marker { display: none; }
94
+ details.commit-selector > summary::after {
95
+ content: '▾';
96
+ float: right;
97
+ margin: 0.5rem 0.85rem;
98
+ color: #16a34a;
99
+ font-size: 0.85rem;
100
+ }
101
+ details.commit-selector[open] > summary::after { content: '▴'; }
102
+ details.commit-selector .dropdown-body {
103
+ margin-top: 0.25rem;
104
+ border: 1px solid var(--rule);
105
+ border-radius: var(--radius);
106
+ padding: 0.25rem;
107
+ }
108
+ details.commit-selector .dropdown-body .row { padding: 0.45rem 0.6rem; }
109
+ details.commit-selector .dropdown-body .row.selected {
110
+ background: rgba(22, 163, 74, 0.07);
111
+ }
112
+ details.commit-selector .dropdown-body .row.selected .kind::after {
113
+ content: ' ●';
114
+ color: #16a34a;
115
+ }
116
+
117
+ /* Tucked-away secondary "storage chunks" list at the bottom of the
118
+ repo view — a click away when you want to see the bytes underneath. */
119
+ details.other-storage {
120
+ margin: 1.25rem 0 0.5rem;
121
+ border-top: 1px solid var(--rule);
122
+ padding-top: 0.5rem;
123
+ }
124
+ details.other-storage > summary {
125
+ cursor: pointer;
126
+ font-size: 0.85rem;
127
+ color: var(--ink-dim);
128
+ padding: 0.35rem 0.25rem;
129
+ }
130
+ details.other-storage[open] > summary { color: var(--ink); }
131
+
132
+ /* "What this is" banner — top of every value tab. Default neutral
133
+ border for storage codecs; green .verified for commits or sigs
134
+ backed by a valid signature; dim .unsigned for commits awaiting
135
+ a signature. */
136
+ .kind-banner {
137
+ display: flex; align-items: center; gap: 0.5rem;
138
+ padding: 0.65rem 0.85rem; margin: 0.5rem 0 1rem;
139
+ border: 1.5px solid var(--rule); border-radius: var(--radius);
140
+ }
141
+ .kind-banner.verified {
142
+ border-color: #16a34a;
143
+ background: rgba(22, 163, 74, 0.06);
144
+ }
145
+ .kind-banner.unsigned { border-style: dashed; }
146
+ .kind-banner .kind-label {
147
+ font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em;
148
+ font-weight: 600; color: var(--ink-dim);
149
+ }
150
+ .kind-banner.verified .kind-label { color: #16a34a; }
151
+ .commit-card {
152
+ padding: 0.6rem 0.85rem; margin: 0.4rem 0;
153
+ border: 1px solid var(--rule); border-radius: var(--radius);
154
+ }
155
+ .commit-card .commit-msg { font-size: 0.95rem; margin-bottom: 0.25rem; }
156
+ .commit-card .commit-meta { font-size: 0.8rem; }
157
+
158
+ .verify-badge { font-weight: 700; padding-left: 0.35em; font-size: 0.95em; }
159
+ .verify-badge.valid { color: #16a34a; }
160
+ .verify-badge.invalid { color: #dc2626; }
161
+ .verify-badge.pending { color: var(--ink-dim); font-weight: 400; }
162
+ .verify-badge.error { color: #ca8a04; }
54
163
 
55
164
  .empty { color: var(--ink-dim); padding: 0.5rem 0.75rem; font-size: 0.9rem; }
56
165
 
@@ -87,11 +196,111 @@
87
196
  h3 { font-size: 0.9rem; font-weight: 600; margin: 1.25rem 0 0.5rem; }
88
197
  h3 .dim { font-weight: 400; font-size: 0.85rem; }
89
198
 
199
+ .explainer {
200
+ font-size: 0.85rem;
201
+ line-height: 1.55;
202
+ color: var(--ink-dim);
203
+ border-left: 2px solid var(--rule);
204
+ padding: 0.4rem 0 0.4rem 0.85rem;
205
+ margin: 0.6rem 0 0.9rem;
206
+ }
207
+ .explainer strong { color: var(--ink); }
208
+
90
209
  .conn { font-size: 0.75rem; color: var(--ink-dim); margin-bottom: 1.5rem; }
91
210
  .conn.ok { color: #16a34a; }
92
211
  .conn.err { color: #dc2626; }
93
212
 
94
- .keyfull { font-family: monospace; font-size: 0.78rem; color: var(--ink-dim); word-break: break-all; }
213
+ .keyfull { font-size: 0.78rem; color: var(--ink-dim); word-break: break-all; }
214
+ .keyfull .mono { font-family: monospace; }
215
+ .repo-link {
216
+ font-family: monospace;
217
+ color: var(--accent);
218
+ cursor: pointer;
219
+ text-decoration: underline dotted;
220
+ }
221
+ .repo-link:hover { background: var(--flash); text-decoration-style: solid; }
222
+
223
+ /* Byte stream — zoomed strip in a horizontally-scrollable container,
224
+ click-drag-to-pan inside for "look around" navigation. */
225
+ .byte-strip-container {
226
+ width: 100%;
227
+ overflow-x: auto;
228
+ background: #faf9f4;
229
+ border: 1.5px solid var(--rule);
230
+ border-radius: var(--radius);
231
+ margin: 0.4rem 0 1rem;
232
+ cursor: grab;
233
+ }
234
+ .byte-strip-container.dragging { cursor: grabbing; user-select: none; }
235
+ .byte-strip-container.dragging .chunk { cursor: grabbing; }
236
+ .byte-strip { display: block; }
237
+
238
+ .byte-map {
239
+ display: block;
240
+ }
241
+ .byte-map .chunk {
242
+ cursor: pointer;
243
+ stroke: rgba(0, 0, 0, 0.15);
244
+ stroke-width: 0.4;
245
+ transition: stroke-width 0.08s, fill-opacity 0.08s;
246
+ }
247
+ .byte-map .chunk:hover { stroke: var(--ink); stroke-width: 1.5; }
248
+ .byte-map .chunk.current { stroke: var(--ink); stroke-width: 2; }
249
+ .byte-map .chunk.hovered { fill-opacity: 0.55; }
250
+
251
+ /* codec category palette — used in both the legend and the SVG fills */
252
+ .cat-commit { fill: #f59e0b; background: #f59e0b; }
253
+ .cat-sig { fill: #ef4444; background: #ef4444; }
254
+ .cat-composite { fill: #3b82f6; background: #3b82f6; }
255
+ .cat-duple { fill: #a855f7; background: #a855f7; }
256
+ .cat-string { fill: #10b981; background: #10b981; }
257
+ .cat-bytes { fill: #84cc16; background: #84cc16; }
258
+ .cat-num { fill: #64748b; background: #64748b; }
259
+ .cat-var { fill: #fbbf24; background: #fbbf24; }
260
+ .cat-other { fill: #cbd5e1; background: #cbd5e1; }
261
+
262
+ .byte-map-legend {
263
+ display: flex;
264
+ gap: 0.5rem;
265
+ flex-wrap: wrap;
266
+ font-size: 0.7rem;
267
+ color: var(--ink-dim);
268
+ margin-top: 0.5rem;
269
+ }
270
+ .byte-map-legend span {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 0.3rem;
274
+ padding: 0.05rem 0.45rem 0.05rem 0.3rem;
275
+ border-radius: 999px;
276
+ color: #fff;
277
+ font-weight: 500;
278
+ letter-spacing: 0.02em;
279
+ }
280
+
281
+ /* Tab strip — hand-drawn underline aesthetic to match proto.css */
282
+ .tabs {
283
+ display: flex;
284
+ gap: 1.25rem;
285
+ border-bottom: 1.5px solid var(--rule);
286
+ margin: 1.25rem 0 1rem;
287
+ }
288
+ .tab {
289
+ padding: 0.45rem 0.1rem;
290
+ margin-bottom: -1.5px;
291
+ border-bottom: 2px solid transparent;
292
+ cursor: pointer;
293
+ font-size: 0.85rem;
294
+ color: var(--ink-dim);
295
+ letter-spacing: 0.04em;
296
+ text-transform: lowercase;
297
+ }
298
+ .tab:hover { color: var(--ink); }
299
+ .tab.active {
300
+ color: var(--ink);
301
+ border-bottom-color: var(--ink);
302
+ font-weight: 600;
303
+ }
95
304
 
96
305
  pre.value {
97
306
  font-family: monospace;