@dtudury/streamo 1.0.1 → 3.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 +25 -17
- package/index.js +32 -0
- package/package.json +16 -2
- package/public/apps/explorer/index.html +118 -0
- package/public/apps/explorer/main.js +434 -0
- package/public/index.html +7 -0
- package/public/streamo/Signer.js +14 -11
- package/public/streamo/Streamo.js +21 -3
- package/public/streamo/registrySync.js +12 -0
- package/public/streamo/utils/Recaller.js +13 -5
- package/.claude/settings.json +0 -126
- package/.claude/settings.local.json +0 -15
- package/.env.dev +0 -4
- package/CLAUDE.md +0 -73
- package/ROADMAP.md +0 -199
- package/jsconfig.json +0 -10
- package/public/streamo/Repo.test.js +0 -82
- package/public/streamo/RepoRegistry.test.js +0 -87
- package/public/streamo/Streamo.test.js +0 -205
- package/public/streamo/h.mount.test.js +0 -67
- package/public/streamo/h.test.js +0 -121
- package/public/streamo/registrySync.test.js +0 -373
- package/public/streamo/sync.test.js +0 -144
- package/public/streamo/utils/mockDOM.js +0 -113
- package/public/streamo/utils/testing.js +0 -90
- package/smoke.test.js +0 -132
package/README.md
CHANGED
|
@@ -56,8 +56,7 @@ streamo --env-file .env
|
|
|
56
56
|
### Streamo — reactive append-only store
|
|
57
57
|
|
|
58
58
|
```js
|
|
59
|
-
import { Streamo } from '@dtudury/streamo
|
|
60
|
-
import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
|
|
59
|
+
import { Streamo } from '@dtudury/streamo'
|
|
61
60
|
|
|
62
61
|
const store = new Streamo()
|
|
63
62
|
store.set({ name: 'alice', score: 42 })
|
|
@@ -72,7 +71,7 @@ Values are encoded with a self-describing codec (strings, numbers, dates, boolea
|
|
|
72
71
|
`Repo` wraps a `Streamo` so every `set()` becomes a commit — message, date, data address, and parent pointer. The raw commit log is what syncs over the wire.
|
|
73
72
|
|
|
74
73
|
```js
|
|
75
|
-
import { Repo } from '@dtudury/streamo
|
|
74
|
+
import { Repo } from '@dtudury/streamo'
|
|
76
75
|
|
|
77
76
|
const repo = new Repo()
|
|
78
77
|
repo.attachSigner(signer, 'my-dataset') // auto-sign every commit
|
|
@@ -87,8 +86,7 @@ Signature chunks travel in the byte stream automatically — peers running `regi
|
|
|
87
86
|
### Signer — deterministic identity
|
|
88
87
|
|
|
89
88
|
```js
|
|
90
|
-
import { Signer } from '@dtudury/streamo
|
|
91
|
-
import { bytesToHex } from '@dtudury/streamo/public/streamo/utils.js'
|
|
89
|
+
import { Signer, bytesToHex } from '@dtudury/streamo'
|
|
92
90
|
|
|
93
91
|
const signer = new Signer('alice', 'my-password')
|
|
94
92
|
const { publicKey } = await signer.keysFor('my-dataset')
|
|
@@ -100,8 +98,7 @@ Keys are derived with PBKDF2 so the same username + password always produces the
|
|
|
100
98
|
### RepoRegistry — multi-repo store
|
|
101
99
|
|
|
102
100
|
```js
|
|
103
|
-
import { RepoRegistry } from '@dtudury/streamo
|
|
104
|
-
import { archiveSync } from '@dtudury/streamo/public/streamo/archiveSync.js'
|
|
101
|
+
import { RepoRegistry, Repo, archiveSync } from '@dtudury/streamo'
|
|
105
102
|
|
|
106
103
|
const registry = new RepoRegistry(async key => {
|
|
107
104
|
const repo = new Repo()
|
|
@@ -115,7 +112,7 @@ const repo = await registry.open(publicKeyHex)
|
|
|
115
112
|
### registrySync — peer sync over WebSocket
|
|
116
113
|
|
|
117
114
|
```js
|
|
118
|
-
import { registrySync } from '@dtudury/streamo
|
|
115
|
+
import { registrySync } from '@dtudury/streamo'
|
|
119
116
|
|
|
120
117
|
const session = await registrySync(registry, 'localhost', 8080, {
|
|
121
118
|
// only sync repos you care about
|
|
@@ -137,9 +134,7 @@ session.announce(myKey, rootKey) // tell interested peers about your repo
|
|
|
137
134
|
### h + mount — reactive UI
|
|
138
135
|
|
|
139
136
|
```js
|
|
140
|
-
import { h } from '@dtudury/streamo
|
|
141
|
-
import { mount } from '@dtudury/streamo/public/streamo/mount.js'
|
|
142
|
-
import { Recaller } from '@dtudury/streamo/public/streamo/utils/Recaller.js'
|
|
137
|
+
import { h, mount, Recaller } from '@dtudury/streamo'
|
|
143
138
|
|
|
144
139
|
const recaller = new Recaller('app')
|
|
145
140
|
|
|
@@ -153,10 +148,13 @@ mount(h`
|
|
|
153
148
|
|
|
154
149
|
Functions interpolated as `${() => ...}` are reactive cells — they re-run automatically whenever the data they read changes. No virtual DOM diffing; only the exact DOM nodes bound to changed data update. Elements are recycled across re-renders by `data-key` (or tag as a fallback), so user input and focus survive list reorders. SVG namespaces propagate automatically — `` h`<svg><path d="..."/></svg>` `` works without any extra wiring. `class` accepts an array (`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`); falsy entries are filtered out.
|
|
155
150
|
|
|
151
|
+
> **For lists that can reorder**, always set `data-key` on each item — the unkeyed positional fallback will recycle elements by tag in document order, which can attach the wrong DOM node (and any user focus/input on it) to the wrong vnode after a reorder.
|
|
152
|
+
|
|
156
153
|
Any function can be used directly as a tag — it receives `{ ...attrs, children }` and returns virtual nodes:
|
|
157
154
|
|
|
158
155
|
```js
|
|
159
|
-
|
|
156
|
+
// StreamoComponent extends HTMLElement, so it's only importable in a browser context:
|
|
157
|
+
import { StreamoComponent, componentKey, defineComponent } from '@dtudury/streamo/StreamoComponent.js'
|
|
160
158
|
|
|
161
159
|
function Card ({ title, children }) {
|
|
162
160
|
return h`<div class="card"><h2>${title}</h2>${children}</div>`
|
|
@@ -179,17 +177,27 @@ For hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn
|
|
|
179
177
|
| `s3Sync` | replicate chunks to S3-compatible object storage |
|
|
180
178
|
| `stateFileSync` | write repo state as JSON on every change |
|
|
181
179
|
|
|
182
|
-
##
|
|
180
|
+
## the all-in-one demo
|
|
181
|
+
|
|
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:
|
|
183
184
|
|
|
184
185
|
```bash
|
|
185
|
-
# start the
|
|
186
|
+
# start the all-in-one demo server
|
|
186
187
|
STREAMO_NAME=my-chat STREAMO_USERNAME=relay STREAMO_PASSWORD=secret \
|
|
187
188
|
node public/apps/chat/server.js
|
|
188
189
|
|
|
189
|
-
#
|
|
190
|
+
# homepage with app cards
|
|
191
|
+
open http://localhost:8080/
|
|
192
|
+
|
|
193
|
+
# chat
|
|
190
194
|
open http://localhost:8080/apps/chat/
|
|
191
195
|
|
|
192
|
-
#
|
|
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
|
|
193
201
|
node public/streamo/chat-cli.js alice secret localhost 8080
|
|
194
202
|
```
|
|
195
203
|
|
|
@@ -198,7 +206,7 @@ Each participant owns their own message stream. The server is just another strea
|
|
|
198
206
|
## tests
|
|
199
207
|
|
|
200
208
|
```bash
|
|
201
|
-
|
|
209
|
+
npm test # all tests
|
|
202
210
|
node --test public/streamo/Repo.test.js # single file
|
|
203
211
|
```
|
|
204
212
|
|
package/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Public API. Most users want named imports from here:
|
|
2
|
+
//
|
|
3
|
+
// import { Repo, Signer, registrySync } from '@dtudury/streamo'
|
|
4
|
+
//
|
|
5
|
+
// For advanced/internal use, subpath imports also work:
|
|
6
|
+
//
|
|
7
|
+
// import { Recaller } from '@dtudury/streamo/utils/Recaller.js'
|
|
8
|
+
//
|
|
9
|
+
// StreamoComponent is intentionally NOT re-exported here: it extends
|
|
10
|
+
// HTMLElement at module-load time and cannot be imported in Node. Browser
|
|
11
|
+
// consumers should subpath-import it directly:
|
|
12
|
+
//
|
|
13
|
+
// import { StreamoComponent, defineComponent } from '@dtudury/streamo/StreamoComponent.js'
|
|
14
|
+
|
|
15
|
+
export { Streamo, ConflictError, changedPaths } from './public/streamo/Streamo.js'
|
|
16
|
+
export { Repo } from './public/streamo/Repo.js'
|
|
17
|
+
export { Signer, verifySignature } from './public/streamo/Signer.js'
|
|
18
|
+
export { Signature } from './public/streamo/Signature.js'
|
|
19
|
+
export { RepoRegistry } from './public/streamo/RepoRegistry.js'
|
|
20
|
+
export { registrySync, handleRegistryPeer } from './public/streamo/registrySync.js'
|
|
21
|
+
export { archiveSync } from './public/streamo/archiveSync.js'
|
|
22
|
+
export { fileSync } from './public/streamo/fileSync.js'
|
|
23
|
+
export { originSync } from './public/streamo/originSync.js'
|
|
24
|
+
export { outletSync, attachStreamSync } from './public/streamo/outletSync.js'
|
|
25
|
+
export { s3Sync } from './public/streamo/s3Sync.js'
|
|
26
|
+
export { stateFileSync } from './public/streamo/stateFileSync.js'
|
|
27
|
+
export { webSync } from './public/streamo/webSync.js'
|
|
28
|
+
export { StreamoServer } from './public/streamo/StreamoServer.js'
|
|
29
|
+
export { h, HElement, HText } from './public/streamo/h.js'
|
|
30
|
+
export { mount, dismount } from './public/streamo/mount.js'
|
|
31
|
+
export { Recaller } from './public/streamo/utils/Recaller.js'
|
|
32
|
+
export { bytesToHex, hexToBytes } from './public/streamo/utils.js'
|
package/package.json
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dtudury/streamo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.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",
|
|
7
7
|
"author": "David Tudury <david.tudury@gmail.com>",
|
|
8
8
|
"license": "AGPL-3.0-only",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./index.js",
|
|
13
|
+
"./*": "./public/streamo/*"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"bin/",
|
|
18
|
+
"public/",
|
|
19
|
+
"!**/*.test.js",
|
|
20
|
+
"!public/streamo/utils/testing.js",
|
|
21
|
+
"!public/streamo/utils/mockDOM.js"
|
|
22
|
+
],
|
|
9
23
|
"bin": {
|
|
10
24
|
"streamo": "./bin/streamo.js"
|
|
11
25
|
},
|
|
12
|
-
"type": "module",
|
|
13
26
|
"dependencies": {
|
|
14
27
|
"@aws-sdk/client-s3": "^3.958.0",
|
|
15
28
|
"@gerhobbelt/gitignore-parser": "^0.2.0-9",
|
|
@@ -22,6 +35,7 @@
|
|
|
22
35
|
"ws": "^8.18.3"
|
|
23
36
|
},
|
|
24
37
|
"scripts": {
|
|
38
|
+
"test": "node --test",
|
|
25
39
|
"serve": "node bin/streamo.js --env-file .env.dev --web 3000 --interactive",
|
|
26
40
|
"chat": "node public/apps/chat/server.js --env-file .env.dev"
|
|
27
41
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
/* 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; }
|
|
37
|
+
|
|
38
|
+
.row .mono { font-size: 0.85rem; }
|
|
39
|
+
.row .when { font-size: 0.78rem; color: var(--ink-dim); }
|
|
40
|
+
.row .msg { font-size: 0.85rem; }
|
|
41
|
+
.row .kind {
|
|
42
|
+
font-size: 0.7rem;
|
|
43
|
+
text-transform: uppercase;
|
|
44
|
+
letter-spacing: 0.08em;
|
|
45
|
+
color: var(--ink-dim);
|
|
46
|
+
border: 1px solid var(--rule);
|
|
47
|
+
border-radius: 999px;
|
|
48
|
+
padding: 0.05rem 0.5rem;
|
|
49
|
+
text-align: center;
|
|
50
|
+
align-self: center;
|
|
51
|
+
}
|
|
52
|
+
.row.commit .kind { color: var(--accent); border-color: var(--accent); }
|
|
53
|
+
.row.signature .kind { color: var(--warn); border-color: var(--warn); }
|
|
54
|
+
|
|
55
|
+
.empty { color: var(--ink-dim); padding: 0.5rem 0.75rem; font-size: 0.9rem; }
|
|
56
|
+
|
|
57
|
+
/* key/value table for the at-view */
|
|
58
|
+
.kv { width: 100%; border-collapse: collapse; font-size: 0.85rem; margin: 0.75rem 0; }
|
|
59
|
+
.kv td { padding: 0.4rem 0.6rem; vertical-align: top; }
|
|
60
|
+
.kv tr + tr td { border-top: 1px dashed var(--rule); }
|
|
61
|
+
.kv td:first-child {
|
|
62
|
+
color: var(--ink-dim);
|
|
63
|
+
width: 8rem;
|
|
64
|
+
font-size: 0.78rem;
|
|
65
|
+
text-transform: uppercase;
|
|
66
|
+
letter-spacing: 0.06em;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* clickable variant — whole row is the click target */
|
|
70
|
+
.kv.clickable tr { cursor: pointer; }
|
|
71
|
+
.kv.clickable tr:hover td { background: rgba(254, 240, 138, 0.4); }
|
|
72
|
+
.kv.clickable td:last-child { color: var(--accent); text-align: right; }
|
|
73
|
+
|
|
74
|
+
.addr-link {
|
|
75
|
+
font-family: monospace;
|
|
76
|
+
font-size: 0.85rem;
|
|
77
|
+
color: var(--accent);
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
text-decoration: underline dotted;
|
|
80
|
+
}
|
|
81
|
+
.addr-link:hover { background: var(--flash); text-decoration-style: solid; }
|
|
82
|
+
|
|
83
|
+
.paths { list-style: none; padding: 0; }
|
|
84
|
+
.paths li { padding: 0.2rem 0.5rem; font-size: 0.85rem; }
|
|
85
|
+
.paths li + li { border-top: 1px dashed var(--rule); }
|
|
86
|
+
|
|
87
|
+
h3 { font-size: 0.9rem; font-weight: 600; margin: 1.25rem 0 0.5rem; }
|
|
88
|
+
h3 .dim { font-weight: 400; font-size: 0.85rem; }
|
|
89
|
+
|
|
90
|
+
.conn { font-size: 0.75rem; color: var(--ink-dim); margin-bottom: 1.5rem; }
|
|
91
|
+
.conn.ok { color: #16a34a; }
|
|
92
|
+
.conn.err { color: #dc2626; }
|
|
93
|
+
|
|
94
|
+
.keyfull { font-family: monospace; font-size: 0.78rem; color: var(--ink-dim); word-break: break-all; }
|
|
95
|
+
|
|
96
|
+
pre.value {
|
|
97
|
+
font-family: monospace;
|
|
98
|
+
font-size: 0.8rem;
|
|
99
|
+
background: var(--rule);
|
|
100
|
+
border-radius: var(--radius);
|
|
101
|
+
padding: 1rem;
|
|
102
|
+
overflow-x: auto;
|
|
103
|
+
white-space: pre-wrap;
|
|
104
|
+
word-break: break-word;
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
</head>
|
|
108
|
+
<body>
|
|
109
|
+
<div class="header">
|
|
110
|
+
<div class="wordmark">streamo</div>
|
|
111
|
+
<div class="crumbs">explorer</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="conn" class="conn">connecting…</div>
|
|
114
|
+
<div id="app"></div>
|
|
115
|
+
|
|
116
|
+
<script type="module" src="./main.js"></script>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|