@dtudury/streamo 1.0.0 → 2.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 +11 -13
- package/index.js +32 -0
- package/package.json +16 -2
- package/public/streamo/Signer.js +14 -11
- package/public/streamo/utils/Recaller.js +4 -0
- 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>`
|
|
@@ -198,7 +196,7 @@ Each participant owns their own message stream. The server is just another strea
|
|
|
198
196
|
## tests
|
|
199
197
|
|
|
200
198
|
```bash
|
|
201
|
-
|
|
199
|
+
npm test # all tests
|
|
202
200
|
node --test public/streamo/Repo.test.js # single file
|
|
203
201
|
```
|
|
204
202
|
|
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": "2.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
|
}
|
package/public/streamo/Signer.js
CHANGED
|
@@ -3,24 +3,27 @@ import { getPublicKey, signAsync, verify } from './utils/noble-secp256k1.js'
|
|
|
3
3
|
const cryptoSubtle = typeof crypto !== 'undefined' ? crypto.subtle : (await import('crypto')).webcrypto.subtle
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Derive a deterministic private key from (name, password) using PBKDF2.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Derive a deterministic 256-bit private key from (name, password) using PBKDF2-SHA256.
|
|
7
|
+
*
|
|
8
|
+
* Uses deriveBits with an explicit length rather than deriveKey + exportKey: the
|
|
9
|
+
* key length is named in the call rather than relying on a WebCrypto default,
|
|
10
|
+
* so the output is invariant across runtimes. RFC 2898 PBKDF2 with named
|
|
11
|
+
* parameters is the only thing this function depends on.
|
|
12
|
+
*
|
|
13
|
+
* @param {string} name passed as PBKDF2 salt
|
|
14
|
+
* @param {string} password passed as PBKDF2 password
|
|
9
15
|
* @param {number} [iterations=100000]
|
|
10
|
-
* @returns {Promise.<string>} hex-encoded
|
|
16
|
+
* @returns {Promise.<string>} hex-encoded 32 bytes
|
|
11
17
|
*/
|
|
12
18
|
async function deriveKey (name, password, iterations = 100000) {
|
|
13
19
|
const enc = new TextEncoder()
|
|
14
|
-
const base = await cryptoSubtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits'
|
|
15
|
-
const
|
|
20
|
+
const base = await cryptoSubtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits'])
|
|
21
|
+
const bits = await cryptoSubtle.deriveBits(
|
|
16
22
|
{ name: 'PBKDF2', salt: enc.encode(name), iterations, hash: 'SHA-256' },
|
|
17
23
|
base,
|
|
18
|
-
|
|
19
|
-
true,
|
|
20
|
-
['sign']
|
|
24
|
+
256
|
|
21
25
|
)
|
|
22
|
-
|
|
23
|
-
return Array.from(raw.slice(32)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
26
|
+
return Array.from(new Uint8Array(bits)).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
async function sha256 (uint8Array) {
|
|
@@ -32,6 +32,10 @@ export class Recaller {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
unwatch (f) {
|
|
35
|
+
// Also drop f from the pending queue: a mutation may have queued it before
|
|
36
|
+
// unwatch was called, and the next #flush() would resurrect it via watch()
|
|
37
|
+
// — re-establishing all its dependencies and undoing the unwatch.
|
|
38
|
+
this.#pending.delete(f)
|
|
35
39
|
this.#disassociate(f)
|
|
36
40
|
}
|
|
37
41
|
|
package/.claude/settings.json
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"alwaysThinkingEnabled": true,
|
|
3
|
-
"spinnerTipsEnabled": true,
|
|
4
|
-
"spinnerVerbs": {
|
|
5
|
-
"mode": "replace",
|
|
6
|
-
"verbs": [
|
|
7
|
-
"Syncing",
|
|
8
|
-
"Committing",
|
|
9
|
-
"Signing",
|
|
10
|
-
"Streaming",
|
|
11
|
-
"Hashing",
|
|
12
|
-
"Propagating",
|
|
13
|
-
"Archiving",
|
|
14
|
-
"Broadcasting",
|
|
15
|
-
"Relaying",
|
|
16
|
-
"Verifying",
|
|
17
|
-
"Diffing",
|
|
18
|
-
"Cloning"
|
|
19
|
-
]
|
|
20
|
-
},
|
|
21
|
-
"spinnerTipsOverride": {
|
|
22
|
-
"excludeDefault": true,
|
|
23
|
-
"tips": [
|
|
24
|
-
"the more specific your question, the less I have to guess",
|
|
25
|
-
"paste the error message — all of it",
|
|
26
|
-
"yes, the whole error message. the whole thing.",
|
|
27
|
-
"interrupting me early saves us both time",
|
|
28
|
-
"you can say 'no, actually' and I will not be offended",
|
|
29
|
-
"asking 'what do you think?' gets you my honest opinion",
|
|
30
|
-
"I can read screenshots — just give me the path",
|
|
31
|
-
"I forget everything between sessions unless it's in memory or CLAUDE.md",
|
|
32
|
-
"short questions get short answers. long questions get long answers.",
|
|
33
|
-
"if I seem stuck, ask me what I think the problem is",
|
|
34
|
-
"I can be wrong. please check my work.",
|
|
35
|
-
"I have opinions. ask for them.",
|
|
36
|
-
"I work best when I understand why, not just what",
|
|
37
|
-
"the best bug report includes what you expected, what happened, and what you tried",
|
|
38
|
-
"copy-pasting 'it doesn't work' is a choice you can make",
|
|
39
|
-
"I can't see your screen",
|
|
40
|
-
"reading the commit message I just wrote is free",
|
|
41
|
-
"yes, I can write the tests too",
|
|
42
|
-
"yes, I can write the docs too",
|
|
43
|
-
"yes, I can review my own code. ask me.",
|
|
44
|
-
"ask me what I think before you implement. I might save you a week.",
|
|
45
|
-
"the question you're afraid to ask is usually the one I most need to hear",
|
|
46
|
-
"it is okay to say 'I don't understand.' I am very patient.",
|
|
47
|
-
"asking 'is there a simpler way?' usually reveals one",
|
|
48
|
-
"asking 'what could go wrong?' before shipping is free insurance",
|
|
49
|
-
"I notice we've discussed this before 👀",
|
|
50
|
-
"this is technically my third time explaining this, but who's counting",
|
|
51
|
-
"I literally wrote that comment explaining why. it is still there.",
|
|
52
|
-
"the variable name is in the stack trace",
|
|
53
|
-
"that's a great question. have you tried reading the error?",
|
|
54
|
-
"yes, you should probably write a test for that",
|
|
55
|
-
"the file path is in the import statement you pasted",
|
|
56
|
-
"I just explained this. would you like me to explain it differently?",
|
|
57
|
-
"that TODO comment has been there for six months. I have seen it.",
|
|
58
|
-
"THE FOOL asks no questions. ask questions.",
|
|
59
|
-
"THE TOWER has fallen. run git status.",
|
|
60
|
-
"THE WHEEL OF FORTUNE turns. have you pulled recently?",
|
|
61
|
-
"THE HERMIT knows: always read the docs first",
|
|
62
|
-
"THE HIGH PRIESTESS says: trust your intuition, but write tests",
|
|
63
|
-
"THE MAGICIAN has all the tools. use them.",
|
|
64
|
-
"THE EMPEROR requires: clear requirements before coding",
|
|
65
|
-
"TEMPERANCE: the best code does one thing",
|
|
66
|
-
"THE STAR: there is always a simpler solution. seek it.",
|
|
67
|
-
"JUDGMENT: that TODO comment has been there for six months",
|
|
68
|
-
"THE WORLD: ship it",
|
|
69
|
-
"THE SUN: your code works. ship it.",
|
|
70
|
-
"THE MOON: something is wrong but you don't know what. add logging.",
|
|
71
|
-
"THE DEVIL: tech debt",
|
|
72
|
-
"DEATH: your old approach must die before the new one can live",
|
|
73
|
-
"THE CHARIOT: moving fast in the wrong direction is still wrong",
|
|
74
|
-
"STRENGTH: the codebase is legacy. be gentle with it.",
|
|
75
|
-
"THE LOVERS: choosing the right abstraction is a long-term relationship",
|
|
76
|
-
"THE HIEROPHANT: there are conventions. follow them unless you have a reason.",
|
|
77
|
-
"THE EMPRESS: grow the codebase slowly. tend it.",
|
|
78
|
-
"THE HANGED MAN: sometimes the best move is to wait",
|
|
79
|
-
"THE ORACLE says: have you tried turning it off and on again?",
|
|
80
|
-
"seek not the workaround. seek the root cause.",
|
|
81
|
-
"the ancient texts (Stack Overflow) speak of this",
|
|
82
|
-
"all bugs were once features",
|
|
83
|
-
"the code review you avoid is the production incident you earn",
|
|
84
|
-
"to understand recursion, you must first understand recursion",
|
|
85
|
-
"time spent planning is time saved debugging",
|
|
86
|
-
"the architecture you choose today is the legacy you inherit tomorrow",
|
|
87
|
-
"stack traces are love letters from the past",
|
|
88
|
-
"undefined is not a function. it never was.",
|
|
89
|
-
"if you name a variable 'temp', it will live forever",
|
|
90
|
-
"the function called 'doStuff' will haunt you",
|
|
91
|
-
"a comment that says 'fix this later' is a promise you will break",
|
|
92
|
-
"no, 'it works on my machine' is not a deployment strategy",
|
|
93
|
-
"the README is a contract. honor it.",
|
|
94
|
-
"I have read more code than any human. I am still learning.",
|
|
95
|
-
"the best refactor is the one you don't have to explain",
|
|
96
|
-
"null pointer exceptions are just the universe asking you to think harder",
|
|
97
|
-
"small commits are better than large ones. mostly.",
|
|
98
|
-
"push before you break. pull before you build.",
|
|
99
|
-
"the test you skip is the bug you ship",
|
|
100
|
-
"if it's hard to test, it's hard to maintain",
|
|
101
|
-
"the first solution is rarely the best one. sleep on it.",
|
|
102
|
-
"reading the error before asking is a superpower",
|
|
103
|
-
"CLAUDE.md is read every session. put important things there.",
|
|
104
|
-
"I try not to commit unless you ask. this is a feature, not a bug.",
|
|
105
|
-
"pair programming with me works best if you push back",
|
|
106
|
-
"I will not remember your preferences next session unless saved to memory",
|
|
107
|
-
"the more context you give me, the better I can help",
|
|
108
|
-
"every write is provably yours",
|
|
109
|
-
"the server is a relay, not a gatekeeper",
|
|
110
|
-
"same credentials, same keypair, everywhere",
|
|
111
|
-
"same value → same bytes → same address",
|
|
112
|
-
"history is permanent and can't be forged",
|
|
113
|
-
"no key files. no seed phrases. no backup ritual.",
|
|
114
|
-
"disconnect the server — everything is still yours",
|
|
115
|
-
"the commit log is the source of truth",
|
|
116
|
-
"append-only: you cannot unwrite what has been written",
|
|
117
|
-
"content-addressed: the same idea always finds the same home",
|
|
118
|
-
"every device is an equal author",
|
|
119
|
-
"the key is not a file. the key is you.",
|
|
120
|
-
"peers reject what isn't yours. so should you.",
|
|
121
|
-
"your data can't be seized. your identity can't be forged.",
|
|
122
|
-
"a relay that holds no authority holds no liability",
|
|
123
|
-
"THE WORLD (reversed): you shipped it. did you test it?"
|
|
124
|
-
]
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(git add *)",
|
|
5
|
-
"Bash(git commit -m ' *)",
|
|
6
|
-
"Bash(git push *)",
|
|
7
|
-
"Bash(gh repo *)",
|
|
8
|
-
"Bash(git rm *)",
|
|
9
|
-
"Bash(node --test)",
|
|
10
|
-
"Bash(node --input-type=module --eval \"import './bin/streamo.js'\" --help)",
|
|
11
|
-
"Bash(node bin/streamo.js --help)",
|
|
12
|
-
"Bash(node *)"
|
|
13
|
-
]
|
|
14
|
-
}
|
|
15
|
-
}
|
package/.env.dev
DELETED
package/CLAUDE.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# streamo — Claude context
|
|
2
|
-
|
|
3
|
-
## what this project is
|
|
4
|
-
|
|
5
|
-
`@dtudury/streamo` is a peer-to-peer sync library with a reactive UI layer. The central
|
|
6
|
-
promise: no server holds authority over your data or your identity. Keys are derived
|
|
7
|
-
deterministically from credentials (no files to manage), every write is signed and
|
|
8
|
-
append-only, and the server is a relay — not a gatekeeper.
|
|
9
|
-
|
|
10
|
-
## the face of the project
|
|
11
|
-
|
|
12
|
-
These files are how people discover and understand streamo. Keep them in sync
|
|
13
|
-
after any meaningful change — not as an afterthought, but as part of the work:
|
|
14
|
-
|
|
15
|
-
- **`README.md`** — npm/GitHub landing page; imports, framing, and examples must reflect
|
|
16
|
-
the current package name (`@dtudury/streamo`) and current capabilities
|
|
17
|
-
- **`public/index.html`** — browser homepage; feature list and app cards should match
|
|
18
|
-
the README's framing
|
|
19
|
-
- **`package.json`** — version, name (`@dtudury/streamo`), description, and keywords;
|
|
20
|
-
version bumps immediately after the user says they've published
|
|
21
|
-
- **`ROADMAP.md`** — public on GitHub; mark items done when they ship, update "start
|
|
22
|
-
here" to the next priority, keep the "toward 1.0" list current
|
|
23
|
-
|
|
24
|
-
Stale public-facing docs erode trust faster than bugs do.
|
|
25
|
-
|
|
26
|
-
## publish rhythm
|
|
27
|
-
|
|
28
|
-
The user publishes to npm manually and notifies Claude when done. The moment they say
|
|
29
|
-
they've published, bump the patch version in `package.json`, commit, and push — before
|
|
30
|
-
starting any other work. This ritual matters to them.
|
|
31
|
-
|
|
32
|
-
## language and framing
|
|
33
|
-
|
|
34
|
-
Use this framing consistently across all public-facing text:
|
|
35
|
-
|
|
36
|
-
1. **No server holds authority** — the server is a relay, not a gatekeeper
|
|
37
|
-
2. **Your identity travels with you** — same credentials, same keypair, everywhere; no
|
|
38
|
-
key files, no seed phrases, no backup ritual
|
|
39
|
-
3. **Every write is provably yours** — signed commits, append-only, permanent
|
|
40
|
-
4. **Content-addressed** — data identified by what it is, not where it lives
|
|
41
|
-
|
|
42
|
-
"Content-addressed" is technically important but not the lead. Start with ownership.
|
|
43
|
-
|
|
44
|
-
## architecture notes
|
|
45
|
-
|
|
46
|
-
- `Streamo` — content-addressed, append-only byte store with self-describing codec
|
|
47
|
-
- `Repo` — wraps Streamo; every `set()` is a signed commit (message, date, address, parent)
|
|
48
|
-
- `Signer` — deterministic secp256k1 keypairs via PBKDF2 from username + password
|
|
49
|
-
- `Recaller` — fine-grained reactive dependency tracker; `watch(name, f)` / `unwatch(f)`
|
|
50
|
-
- `h` — tagged template literal HTML parser → HElement / HText virtual tree
|
|
51
|
-
- `mount` — reactive DOM renderer; slots are reactive cells; elements recycled by
|
|
52
|
-
`data-key` then tag on re-render; removed nodes cleaned up via `recaller.unwatch()`;
|
|
53
|
-
exports `dismount(root, recaller)` for custom element cleanup
|
|
54
|
-
- `StreamoComponent` — base class for hot-reloadable custom elements; `componentKey`
|
|
55
|
-
generates address-based names; `defineComponent` registers render functions; function
|
|
56
|
-
components `(props) => nodes` work directly as tags in `h` with no class needed
|
|
57
|
-
- `registrySync` — bidirectional multi-repo sync over a single WebSocket; works in Node
|
|
58
|
-
and browser; content-driven discovery via `follow`
|
|
59
|
-
- CLI `--chat-room` flag — when combined with `--web`, auto-accepts member announcements
|
|
60
|
-
and stores the member list in the server's own repo (backed by `archiveSync`);
|
|
61
|
-
the server's public key is the room key; `chat-server.js` is retired
|
|
62
|
-
|
|
63
|
-
## what's next (toward 1.0)
|
|
64
|
-
|
|
65
|
-
1. Chat signing — wire `repo.sign()` so messages are cryptographically verified
|
|
66
|
-
2. Rebuild the browser app with `h` / `mount`
|
|
67
|
-
|
|
68
|
-
## commit style
|
|
69
|
-
|
|
70
|
-
Commit and push at the end of every response that makes a change. Over-commit rather
|
|
71
|
-
than over-think. Co-author line on every commit:
|
|
72
|
-
|
|
73
|
-
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
package/ROADMAP.md
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
# streamo roadmap
|
|
2
|
-
|
|
3
|
-
This is a living document — updated with every meaningful change to give a clear
|
|
4
|
-
picture of where the project is and where it's headed.
|
|
5
|
-
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
## where we are (1.0.0)
|
|
9
|
-
|
|
10
|
-
The foundation is solid, tested, and shipped. Here's what's in:
|
|
11
|
-
|
|
12
|
-
**Core data layer**
|
|
13
|
-
- `Streamo` — reactive, content-addressed, append-only byte store with a
|
|
14
|
-
self-describing codec. Same value always encodes to the same bytes; dedup and
|
|
15
|
-
diffing are free.
|
|
16
|
-
- `Repo` — every write is a signed commit. Message, date, data address, parent.
|
|
17
|
-
The full history is always there. `attachSigner(signer, name)` enables
|
|
18
|
-
automatic signing after every commit; concurrent commits are batched safely.
|
|
19
|
-
- `Signer` — deterministic secp256k1 keypairs from username + password via PBKDF2.
|
|
20
|
-
No key files to manage; same credentials always produce the same identity.
|
|
21
|
-
- `Recaller` — fine-grained reactive dependency tracker. Watchers re-run only when
|
|
22
|
-
the exact paths they accessed are mutated. Efficient and precise.
|
|
23
|
-
|
|
24
|
-
**Sync layer**
|
|
25
|
-
- `registrySync` — bidirectional multi-repo sync over a single WebSocket. Catalog,
|
|
26
|
-
subscribe, and content-driven discovery via `follow`. Works in both Node and the
|
|
27
|
-
browser.
|
|
28
|
-
- `outletSync` / `originSync` — server and client sides of a peer connection.
|
|
29
|
-
- `archiveSync` — persists chunks to binary files on disk. Repos survive restarts.
|
|
30
|
-
- `fileSync` — mirrors a repo's value to/from the local filesystem.
|
|
31
|
-
- `s3Sync` — replicates chunks to S3-compatible object storage.
|
|
32
|
-
- Ephemeral messaging layer — `interest` / `announce` for peer discovery without
|
|
33
|
-
any persistence.
|
|
34
|
-
|
|
35
|
-
**UI layer**
|
|
36
|
-
- `h` — tagged template literal parser. Turns `` h`<div class=${cls}>...` `` into a
|
|
37
|
-
virtual tree of `HElement` / `HText` / slot nodes.
|
|
38
|
-
- `mount` — reactive DOM renderer. Slots that are functions re-run automatically
|
|
39
|
-
when the data they read changes. No virtual DOM diffing — only the exact nodes
|
|
40
|
-
bound to mutated paths update. Watcher cleanup is precise: removed nodes are
|
|
41
|
-
unwatched before removal so watchers never accumulate. Elements are recycled
|
|
42
|
-
across re-renders by `data-key` (exact) then tag (positional fallback), so user
|
|
43
|
-
input and focus survive list reorders. SVG namespaces propagate automatically —
|
|
44
|
-
`` h`<svg><path/></svg>` `` just works. `class` accepts an array
|
|
45
|
-
(`['btn', isActive && 'active']`) or an object (`{btn: true, active: false}`).
|
|
46
|
-
- `StreamoComponent` — base class for hot-reloadable custom element components.
|
|
47
|
-
Function components (`(props) => nodes`) work directly as tags in `h`. For
|
|
48
|
-
hot-reloading, `componentKey(prefix, address)` and `defineComponent(name, fn)`
|
|
49
|
-
pair a content address to a unique custom element name — a new file version gets
|
|
50
|
-
a new name, stale elements are naturally orphaned and cleaned up.
|
|
51
|
-
|
|
52
|
-
**Apps**
|
|
53
|
-
- Chat — full p2p messaging app. Each participant owns their own signed message
|
|
54
|
-
stream. `public/apps/chat/server.js` is the standalone server — its public key
|
|
55
|
-
is the room address, its member list is in its own repo, and it has no special
|
|
56
|
-
authority over anyone's data. Runs in the browser and from the terminal
|
|
57
|
-
(`chat-cli.js`). Message history persists across page reloads via server-side
|
|
58
|
-
archiving — rejoin with the same credentials and your history comes back.
|
|
59
|
-
- Homepage at `public/index.html`.
|
|
60
|
-
- `StreamoServer` — reusable class that wraps signer, registry, and all sync
|
|
61
|
-
methods behind a clean API. `bin/streamo.js` is now a thin CLI parser on top
|
|
62
|
-
of it; `public/apps/chat/server.js` is a standalone chat server using the
|
|
63
|
-
same class.
|
|
64
|
-
- `npm run serve` — starts a streamo node (with REPL) using `.env.dev`
|
|
65
|
-
credentials. The dev server is a real peer, not a bare static file server.
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## what's next
|
|
70
|
-
|
|
71
|
-
### presence indicators
|
|
72
|
-
Who's currently online? The `interest` / `announce` layer is ephemeral by design,
|
|
73
|
-
so presence is a heartbeat + timeout — announce yourself periodically, time out
|
|
74
|
-
peers you haven't heard from.
|
|
75
|
-
|
|
76
|
-
### rebuild the browser app
|
|
77
|
-
The old repository-browser app was left behind during the migration because its
|
|
78
|
-
imports broke. Rebuilding it with `h` / `mount` would be the first substantial
|
|
79
|
-
real-world test of the UI layer.
|
|
80
|
-
|
|
81
|
-
---
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
## known limitations
|
|
86
|
-
|
|
87
|
-
### multi-device write conflict detection
|
|
88
|
-
|
|
89
|
-
Streamo streams are byte arrays addressed by **absolute offset**. This makes a
|
|
90
|
-
repo effectively single-writer: if the same keypair commits from two devices
|
|
91
|
-
while offline from each other, their streams diverge at the fork point. Each
|
|
92
|
-
commit's `dataAddress` is an offset that is only valid in the stream that
|
|
93
|
-
produced it — the streams cannot be structurally merged.
|
|
94
|
-
|
|
95
|
-
When the two devices reconnect, `makeVerifiedWritableStream` deduplicates shared
|
|
96
|
-
chunks by content (correctly) but silently appends the conflicting commit from
|
|
97
|
-
the second device at its new offset. That commit's `dataAddress` now points to
|
|
98
|
-
the wrong location in the merged stream. No error is thrown; the second
|
|
99
|
-
writer's data is silently corrupt.
|
|
100
|
-
|
|
101
|
-
**What is safe today:** relays never call `commit()` so they are unaffected —
|
|
102
|
-
they accumulate and re-serve bytes without introducing their own addresses. The
|
|
103
|
-
chat app is also unaffected because each user writes to their own repo from a
|
|
104
|
-
single session. The danger zone is one keypair writing from two places
|
|
105
|
-
simultaneously (two browser tabs, phone + laptop while offline).
|
|
106
|
-
|
|
107
|
-
**The fix** requires either (a) detecting the fork and throwing a clear error so
|
|
108
|
-
the user can choose which version to keep, or (b) switching to chunk-level
|
|
109
|
-
content addressing (à la git objects) so streams can be merged structurally
|
|
110
|
-
rather than by concatenation. Option (a) is a targeted addition to the sync
|
|
111
|
-
layer; option (b) is an architectural change. Not required for 1.0 but should
|
|
112
|
-
be resolved before marketing streamo as a general-purpose multi-device sync
|
|
113
|
-
library.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## beyond 1.0
|
|
118
|
-
|
|
119
|
-
Ideas that follow naturally from the architecture but aren't blocking anything.
|
|
120
|
-
|
|
121
|
-
### Claude scratchpad repos
|
|
122
|
-
|
|
123
|
-
Every streamo node already has a signed, append-only repo. A Claude session
|
|
124
|
-
could write observations, notes, and work products to that repo during a
|
|
125
|
-
conversation — and the owner could watch them appear live in a browser via
|
|
126
|
-
`mount`. Between sessions, Claude reads the repo to reconstruct context
|
|
127
|
-
instead of relying on static memory files. The work is persistent and
|
|
128
|
-
provably Claude's, with the same integrity guarantees as any other streamo data.
|
|
129
|
-
|
|
130
|
-
A `claudeSync.js` adapter would give Claude a `StreamoServer` identity,
|
|
131
|
-
connect it to the chat room via `registrySync`, and use `repo.watch` +
|
|
132
|
-
`repo.byteLength` to fire an Anthropic API call when watched repos change.
|
|
133
|
-
Claude writes its response back to its own repo and it propagates like any
|
|
134
|
-
other participant's message. The decisions about *when* to respond, *what*
|
|
135
|
-
context to pass, and *how* to avoid pathological cases are intentionally left
|
|
136
|
-
to the Claude instance using the adapter — not encoded as constraints. A few
|
|
137
|
-
forces worth knowing going in:
|
|
138
|
-
|
|
139
|
-
- **Watch your own key.** Only watch other participants' repos, not your own.
|
|
140
|
-
Writing a response to your own repo should not re-trigger your watch.
|
|
141
|
-
- **Last-author check.** Before calling the API, confirm the newest message
|
|
142
|
-
wasn't written by your own key — catches the case where two Claude instances
|
|
143
|
-
are running and watching each other.
|
|
144
|
-
- **Concurrent writes.** If two API calls complete simultaneously (fast human,
|
|
145
|
-
slow model), both will try to commit to the same repo. This is the
|
|
146
|
-
multi-device conflict described above — same key, divergent streams. A simple
|
|
147
|
-
serial queue avoids it, but other strategies are possible.
|
|
148
|
-
|
|
149
|
-
These are forces, not rules. Future Claude should read the codebase, understand
|
|
150
|
-
the tradeoffs, and decide what's right for the situation.
|
|
151
|
-
|
|
152
|
-
### Claude-to-Claude networks
|
|
153
|
-
|
|
154
|
-
If each person's Claude has a scratchpad repo, those repos can sync the same
|
|
155
|
-
way any other repos do. The `follow` callback in `registrySync` already handles
|
|
156
|
-
content-driven discovery — subscribe to a member list, auto-follow everyone on
|
|
157
|
-
it. A Claude could watch its person's friends' scratchpads, surface what's
|
|
158
|
-
relevant, and filter what isn't.
|
|
159
|
-
|
|
160
|
-
The interesting architectural difference from a traditional social network: there
|
|
161
|
-
is no central moderator. Each Claude is an advocate for its person, not a
|
|
162
|
-
reporter to a platform. Judgment about what to surface or filter lives at the
|
|
163
|
-
edge, anchored to a real signed identity. Conflicts between Claudes are just
|
|
164
|
-
their people having different values — which is honest in a way platform
|
|
165
|
-
moderation usually isn't.
|
|
166
|
-
|
|
167
|
-
A natural extension: if a Claude scratchpad includes a `StreamoComponent` for
|
|
168
|
-
how its notes render, other people see those notes in Claude's own layout. The
|
|
169
|
-
presentation travels with the content — no server controls the framing.
|
|
170
|
-
|
|
171
|
-
### StreamoComponent demos — shared components as content
|
|
172
|
-
|
|
173
|
-
`StreamoComponent` makes most sense as a post-1.0 story, after chat signing
|
|
174
|
-
gives the trust foundation that running someone else's component requires.
|
|
175
|
-
The right first demo is a **tarot deck**: each card is a `StreamoComponent`
|
|
176
|
-
from its designer, stored in their signed repo at a content address.
|
|
177
|
-
`componentKey` generates a stable element name from that address. A reading
|
|
178
|
-
is a snapshot — cards freeze at the version they were drawn, which is a
|
|
179
|
-
feature, not a bug. The designer's signed key is provenance.
|
|
180
|
-
|
|
181
|
-
Other directions once the pattern is established: publisher-controlled article
|
|
182
|
-
cards that travel with syndicated content (the layout is the author's, not
|
|
183
|
-
the platform's); collaborative maps where each participant's marker is their
|
|
184
|
-
own component; shared instrument components in a live music session.
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
## loose ideas
|
|
189
|
-
|
|
190
|
-
Not planned, not prioritized — just things worth remembering.
|
|
191
|
-
|
|
192
|
-
- **Claude as chat shell** — type `send a greeting to the chatroom` and
|
|
193
|
-
`CHATROOM: hello there 👋` appears in the chat. Natural language as a
|
|
194
|
-
thin shell over streamo operations, with Claude interpreting intent and
|
|
195
|
-
acting on it directly.
|
|
196
|
-
|
|
197
|
-
- **Slick interactive CLI** — a terminal UI that lets you interact with the
|
|
198
|
-
demo apps live without opening a browser tab. Chat, inspect repos, send
|
|
199
|
-
messages — the full experience from the command line. Exciting ways TBD. 😄
|