@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.
@@ -1,82 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { Repo } from './Repo.js'
3
-
4
- describe(import.meta.url, ({ test }) => {
5
- test('commit stores message, date, and a reference to the data', ({ assert }) => {
6
- const repo = new Repo()
7
- const working = repo.checkout()
8
- working.set({ a: 1 })
9
- const commitAddr = repo.commit(working, 'first commit')
10
- const commit = repo.decode(commitAddr)
11
- assert.equal(commit.message, 'first commit')
12
- assert.ok(commit.date instanceof Date)
13
- assert.equal(typeof commit.dataAddress, 'number')
14
- assert.deepEqual(repo.decode(commit.dataAddress), { a: 1 })
15
- })
16
-
17
- test('checkout starts with last committed value', ({ assert }) => {
18
- const repo = new Repo()
19
- const working = repo.checkout()
20
- working.set({ a: 1 })
21
- repo.commit(working, 'first')
22
- const working2 = repo.checkout()
23
- assert.deepEqual(working2.get(), { a: 1 })
24
- })
25
-
26
- test('checkout of empty repo returns empty stream', ({ assert }) => {
27
- const repo = new Repo()
28
- const working = repo.checkout()
29
- assert.equal(working.byteLength, 0)
30
- })
31
-
32
- test('working stream modifications do not affect the repository', ({ assert }) => {
33
- const repo = new Repo()
34
- const working = repo.checkout()
35
- working.set({ v: 1 })
36
- repo.commit(working, 'first')
37
- const working2 = repo.checkout()
38
- working2.set({ v: 99 })
39
- // repo still has v:1 as last committed value
40
- assert.deepEqual(repo.decode(repo.lastCommit.dataAddress), { v: 1 })
41
- })
42
-
43
- test('multiple commits produce a linked history via parent', ({ assert }) => {
44
- const repo = new Repo()
45
- const working = repo.checkout()
46
- working.set({ v: 1 })
47
- repo.commit(working, 'first')
48
- working.set('v', 2)
49
- repo.commit(working, 'second')
50
- const c2 = repo.lastCommit
51
- assert.equal(c2.message, 'second')
52
- assert.deepEqual(repo.decode(c2.dataAddress), { v: 2 })
53
- const c1 = repo.decode(c2.parent)
54
- assert.equal(c1.message, 'first')
55
- assert.deepEqual(repo.decode(c1.dataAddress), { v: 1 })
56
- })
57
-
58
- test('first commit has no parent', ({ assert }) => {
59
- const repo = new Repo()
60
- const working = repo.checkout()
61
- working.set({ x: 1 })
62
- repo.commit(working, 'root')
63
- assert.equal(repo.lastCommit.parent, undefined)
64
- })
65
-
66
- test('unchanged data reuses the same address across commits', ({ assert }) => {
67
- const repo = new Repo()
68
- const working = repo.checkout()
69
- working.set({ x: 42 })
70
- repo.commit(working, 'first')
71
- repo.commit(working, 'second')
72
- const c2 = repo.lastCommit
73
- const c1 = repo.decode(c2.parent)
74
- assert.equal(c1.dataAddress, c2.dataAddress, 'same data reuses the same address')
75
- })
76
-
77
- test('throws when working stream is empty', ({ assert }) => {
78
- const repo = new Repo()
79
- const working = repo.checkout()
80
- assert.throws(() => repo.commit(working, 'nothing here'))
81
- })
82
- })
@@ -1,87 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { Streamo } from './Streamo.js'
3
- import { Repo } from './Repo.js'
4
- import { RepoRegistry } from './RepoRegistry.js'
5
- import { archiveSync } from './archiveSync.js'
6
-
7
- function archiveRegistry (dir) {
8
- return new RepoRegistry(async key => {
9
- const repo = new Repo()
10
- await archiveSync(repo, dir, key)
11
- return repo
12
- })
13
- }
14
-
15
- describe(import.meta.url, ({ test }) => {
16
- test('plain registry creates in-memory repositories with no factory', async ({ assert }) => {
17
- const registry = new RepoRegistry()
18
- const s = await registry.open('anykey')
19
- assert.ok(s instanceof Repo)
20
- s.set({ x: 1 })
21
- assert.equal(s.get('x'), 1)
22
- })
23
-
24
- test('open creates a repository and returns the same instance on repeat calls', async ({ assert }) => {
25
- const registry = new RepoRegistry()
26
- const s1 = await registry.open('aabbcc')
27
- const s2 = await registry.open('aabbcc')
28
- assert.ok(s1 === s2, 'same instance returned')
29
- assert.equal(registry.size, 1)
30
- })
31
-
32
- test('open creates independent repositories for different keys', async ({ assert }) => {
33
- const registry = new RepoRegistry()
34
- const s1 = await registry.open('key1')
35
- const s2 = await registry.open('key2')
36
- assert.ok(s1 !== s2)
37
- assert.equal(registry.size, 2)
38
- s1.set({ from: 'key1' })
39
- s2.set({ from: 'key2' })
40
- assert.equal(s1.get('from'), 'key1')
41
- assert.equal(s2.get('from'), 'key2')
42
- })
43
-
44
- test('concurrent open() calls return the same instance', async ({ assert }) => {
45
- let created = 0
46
- const registry = new RepoRegistry(async () => {
47
- created++
48
- await new Promise(r => setTimeout(r, 10))
49
- return new Repo()
50
- })
51
- const [s1, s2, s3] = await Promise.all([
52
- registry.open('k'),
53
- registry.open('k'),
54
- registry.open('k')
55
- ])
56
- assert.equal(created, 1, 'factory called only once')
57
- assert.ok(s1 === s2 && s2 === s3, 'all calls return same instance')
58
- })
59
-
60
- test('get returns undefined for unopened or still-opening keys', async ({ assert }) => {
61
- const registry = new RepoRegistry()
62
- assert.equal(registry.get('nope'), undefined)
63
- await registry.open('exists')
64
- assert.ok(registry.get('exists') instanceof Streamo)
65
- })
66
-
67
- test('iterates over fully-opened repositories only', async ({ assert }) => {
68
- const registry = new RepoRegistry()
69
- await registry.open('a')
70
- await registry.open('b')
71
- const entries = [...registry]
72
- assert.equal(entries.length, 2)
73
- assert.deepEqual(entries.map(([k]) => k).sort(), ['a', 'b'])
74
- })
75
-
76
- test('archive factory persists and reloads repository data', async ({ assert }) => {
77
- const dir = '/tmp/repository-registry-persist-test-' + Date.now()
78
- const r1 = archiveRegistry(dir)
79
- const s1 = await r1.open('testkey')
80
- s1.set({ saved: true })
81
- await new Promise(r => setTimeout(r, 50))
82
-
83
- const r2 = archiveRegistry(dir)
84
- const s2 = await r2.open('testkey')
85
- assert.equal(s2.get('saved'), true, 'data survived registry reload')
86
- })
87
- })
@@ -1,205 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { Streamo, ConflictError } from './Streamo.js'
3
- import { Signer } from './Signer.js'
4
- import { Signature } from './Signature.js'
5
-
6
- describe(import.meta.url, ({ test }) => {
7
- test('encodes and decodes primitive values', ({ assert }) => {
8
- const s = new Streamo()
9
- const values = [
10
- undefined, null, false, true,
11
- 0, 1, 127,
12
- 128, -1, 3.14, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY,
13
- new Uint8Array([1, 2, 3]),
14
- new Uint8Array([4, 5, 6, 7]),
15
- new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
16
- 'hello',
17
- 'a longer string that definitely does not fit in four bytes',
18
- new Date('1969-07-21T22:56:15Z'),
19
- { a: 1, b: 2, c: 3 },
20
- { x: 'hello' },
21
- {},
22
- [1, 2, 3],
23
- [],
24
- ['a', 'b'],
25
- new Signature(0, new Uint8Array(64))
26
- ]
27
- for (const value of values) {
28
- const code = s.encodeVariable(value)
29
- const decoded = s.decode(code)
30
- assert.deepEqual(decoded, value, `round-trips ${Object.prototype.toString.call(value)}`)
31
- }
32
- })
33
-
34
- test('negative addresses for single-byte primitives', ({ assert }) => {
35
- const s = new Streamo()
36
- for (const v of [undefined, null, false, true, 0, 1, 127]) {
37
- const code = s.encode(v)
38
- assert.equal(code.length, 1, `${String(v)} encodes to 1 byte`)
39
- const addr = -(code[0] + 1)
40
- assert.ok(addr < 0, `${String(v)} has a negative address`)
41
- assert.deepEqual(s.decode(addr), v, `negative address resolves back to ${String(v)}`)
42
- }
43
- })
44
-
45
- test('deduplication: same value always gets the same address', ({ assert }) => {
46
- const s = new Streamo()
47
- const a1 = s.append(s.encode(42))
48
- s.append(s.encode({ x: 42 }))
49
- const code42 = s.encode(42)
50
- assert.equal(s.addressOf(code42), a1, 'second encode of 42 reuses the existing address')
51
- })
52
-
53
- test('reactive get/set/watch', async ({ assert }) => {
54
- const s = new Streamo()
55
- let callCount = 0
56
- let lastValue
57
-
58
- s.watch('test', () => {
59
- lastValue = s.get('greeting')
60
- callCount++
61
- })
62
- assert.equal(callCount, 1, 'watch runs immediately')
63
- assert.equal(lastValue, undefined, 'no value yet')
64
-
65
- s.set({ greeting: 'hello' })
66
- await new Promise(resolve => setTimeout(resolve, 20))
67
- assert.equal(callCount, 2, 'watch re-ran after set')
68
- assert.equal(lastValue, 'hello', 'updated value seen')
69
-
70
- s.set('greeting', 'world')
71
- await new Promise(resolve => setTimeout(resolve, 20))
72
- assert.equal(callCount, 3, 'watch re-ran after path set')
73
- assert.equal(lastValue, 'world', 'path update seen')
74
- })
75
-
76
- test('asRefs returns addresses for object values and names', ({ assert }) => {
77
- const s = new Streamo()
78
- const code = s.encode({ a: 1 })
79
-
80
- // asRefs=true: values become addresses, names stay as strings
81
- const withTrue = s.decode(code, true)
82
- assert.deepEqual(Object.keys(withTrue), ['a'])
83
- assert.equal(typeof withTrue.a, 'number', 'value is an address')
84
- assert.equal(s.decode(withTrue.a), 1, 'address decodes to original value')
85
-
86
- // asRefs=[true, false]: same — value is address, name is string
87
- const withValueRef = s.decode(code, [true, false])
88
- assert.equal(typeof withValueRef.a, 'number')
89
- assert.equal(s.decode(withValueRef.a), 1)
90
-
91
- // asRefs=[false, true]: value decoded, name is address
92
- const withNameRef = s.decode(code, [false, true])
93
- assert.deepEqual(Object.values(withNameRef), [1])
94
- const nameAddr = Number(Object.keys(withNameRef)[0])
95
- assert.equal(s.decode(nameAddr), 'a', 'key address decodes to the name string')
96
- })
97
-
98
- test('asRefs: object returns name/address map', ({ assert }) => {
99
- const s = new Streamo()
100
- s.set({ x: 1, y: 2 })
101
- const refs = s.asRefs(s.byteLength - 1)
102
- assert.deepEqual(Object.keys(refs), ['x', 'y'])
103
- assert.equal(typeof refs.x, 'number')
104
- assert.equal(typeof refs.y, 'number')
105
- assert.equal(s.decode(refs.x), 1)
106
- assert.equal(s.decode(refs.y), 2)
107
- })
108
-
109
- test('asRefs: array returns element addresses', ({ assert }) => {
110
- const s = new Streamo()
111
- s.set(['a', 'b', 'c'])
112
- const refs = s.asRefs(s.byteLength - 1)
113
- assert.ok(Array.isArray(refs))
114
- assert.equal(refs.length, 3)
115
- refs.forEach(addr => assert.equal(typeof addr, 'number'))
116
- assert.equal(s.decode(refs[0]), 'a')
117
- assert.equal(s.decode(refs[1]), 'b')
118
- assert.equal(s.decode(refs[2]), 'c')
119
- })
120
-
121
- test('asRefs: non-object returns the address itself', ({ assert }) => {
122
- const s = new Streamo()
123
- s.set('hello')
124
- const address = s.byteLength - 1
125
- assert.equal(s.asRefs(address), address)
126
- })
127
-
128
- test('encode(asRefs(addr), true) round-trips an object', ({ assert }) => {
129
- const s = new Streamo()
130
- s.set({ a: 1, b: 'hello' })
131
- const addr = s.byteLength - 1
132
- const refs = s.asRefs(addr)
133
- const code = s.encode(refs, true)
134
- assert.deepEqual(s.decode(code), { a: 1, b: 'hello' })
135
- })
136
-
137
- test('encode(asRefs(addr), true) round-trips an array', ({ assert }) => {
138
- const s = new Streamo()
139
- s.set([10, 20, 30])
140
- const addr = s.byteLength - 1
141
- const refs = s.asRefs(addr)
142
- const code = s.encode(refs, true)
143
- assert.deepEqual(s.decode(code), [10, 20, 30])
144
- })
145
-
146
- test('encode(asRefs(addr), true) round-trips a primitive', ({ assert }) => {
147
- const s = new Streamo()
148
- s.set('hello')
149
- const addr = s.byteLength - 1
150
- const refs = s.asRefs(addr) // returns addr itself for non-objects
151
- const code = s.encode(refs, true) // resolves addr → string code
152
- assert.equal(s.decode(code), 'hello')
153
- })
154
-
155
- test('sign and verify', async ({ assert }) => {
156
- const s = new Streamo()
157
- s.set({ hello: 'world' })
158
- s.set('hello', 'signed')
159
-
160
- const signer = new Signer('alice', 'secret')
161
- const name = 'my-streamo'
162
- const keys = await signer.keysFor(name)
163
- const sig = await s.sign(signer, name)
164
-
165
- assert.ok(sig instanceof Signature)
166
- assert.ok(await s.verify(sig, keys.publicKey), 'signature verifies with correct key')
167
-
168
- const other = new Signer('bob', 'different')
169
- const otherKeys = await other.keysFor(name)
170
- assert.ok(!(await s.verify(sig, otherKeys.publicKey)), 'wrong key does not verify')
171
- })
172
-
173
- test('conditionalSet rejects stale edits and accepts fresh ones', ({ assert }) => {
174
- const s = new Streamo()
175
- s.set({ x: 1 })
176
- const tip = s.byteLength
177
-
178
- // A concurrent write advances the streamo past tip
179
- s.set({ x: 2 })
180
-
181
- // Stale edit is rejected
182
- let caught
183
- try { s.conditionalSet(tip, { x: 3 }) } catch (e) { caught = e }
184
- assert.ok(caught instanceof ConflictError, 'throws ConflictError')
185
- assert.equal(caught.expectedTip, tip)
186
- assert.equal(caught.actualTip, s.byteLength)
187
- assert.equal(s.get('x'), 2, 'streamo unchanged after rejection')
188
-
189
- // Fresh edit at current tip succeeds
190
- const freshTip = s.byteLength
191
- s.conditionalSet(freshTip, { x: 3 })
192
- assert.equal(s.get('x'), 3, 'fresh conditional set applied')
193
- })
194
-
195
- test('clone snapshots state at a given address', ({ assert }) => {
196
- const s = new Streamo()
197
- s.set({ v: 1 })
198
- const addr1 = s.byteLength - 1
199
- s.set({ v: 2 })
200
-
201
- const snap = s.clone(addr1)
202
- assert.equal(snap.get('v'), 1, 'clone reflects state at snapshot address')
203
- assert.equal(s.get('v'), 2, 'original still reflects latest state')
204
- })
205
- })
@@ -1,67 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { h } from './h.js'
3
- import { mount } from './mount.js'
4
- import { Streamo } from './Streamo.js'
5
-
6
- const IS_NODE = typeof process !== 'undefined' && process.versions?.node != null
7
-
8
- if (IS_NODE) {
9
- const { mockDocument, MockNode } = await import('./utils/mockDOM.js')
10
- globalThis.document = mockDocument
11
- globalThis.Node = MockNode
12
- }
13
-
14
- describe(import.meta.url, ({ test }) => {
15
- test('mounts a static element with text', ({ assert }) => {
16
- const container = document.createElement('div')
17
- mount(h`<span>hello</span>`, container)
18
- const span = container.childNodes[0]
19
- assert.equal(span.textContent, 'hello')
20
- })
21
-
22
- test('reactive text slot updates after stream.set', async ({ assert }) => {
23
- const stream = new Streamo()
24
- stream.set({ greeting: 'hello' })
25
- const container = document.createElement('div')
26
- mount(h`<span>${() => stream.get('greeting')}</span>`, container, stream.recaller)
27
-
28
- const span = container.childNodes[0]
29
- assert.equal(span.textContent, 'hello', 'initial render')
30
-
31
- stream.set('greeting', 'world')
32
- await new Promise(resolve => setTimeout(resolve, 20))
33
- assert.equal(span.textContent, 'world', 'updated after set')
34
- })
35
-
36
- test('reactive attribute updates after stream.set', async ({ assert }) => {
37
- const stream = new Streamo()
38
- stream.set({ cls: 'active' })
39
- const container = document.createElement('div')
40
- mount(h`<div class=${() => stream.get('cls')}></div>`, container, stream.recaller)
41
-
42
- const div = container.childNodes[0]
43
- assert.equal(div.getAttribute('class'), 'active', 'initial attribute')
44
-
45
- stream.set('cls', 'inactive')
46
- await new Promise(resolve => setTimeout(resolve, 20))
47
- assert.equal(div.getAttribute('class'), 'inactive', 'updated attribute')
48
- })
49
-
50
- test('unrelated stream.set does not re-render slot', async ({ assert }) => {
51
- const stream = new Streamo()
52
- stream.set({ a: 'unchanged', b: 'watched' })
53
- let renderCount = 0
54
- const container = document.createElement('div')
55
- mount(h`<span>${() => { renderCount++; return stream.get('b') }}</span>`, container, stream.recaller)
56
-
57
- assert.equal(renderCount, 1, 'initial render')
58
-
59
- stream.set('a', 'changed')
60
- await new Promise(resolve => setTimeout(resolve, 20))
61
- assert.equal(renderCount, 1, 'no re-render when unrelated key changes')
62
-
63
- stream.set('b', 'also changed')
64
- await new Promise(resolve => setTimeout(resolve, 20))
65
- assert.equal(renderCount, 2, 're-renders when watched key changes')
66
- })
67
- })
@@ -1,121 +0,0 @@
1
- import { describe } from './utils/testing.js'
2
- import { h, HElement, HText } from './h.js'
3
-
4
- describe(import.meta.url, ({ test }) => {
5
- test('plain element', ({ assert }) => {
6
- const [div] = h`<div></div>`
7
- assert.ok(div instanceof HElement)
8
- assert.equal(div.tag, 'div')
9
- assert.deepEqual(div.attrs, [])
10
- assert.deepEqual(div.children, [])
11
- })
12
-
13
- test('void element produces no children', ({ assert }) => {
14
- const [br] = h`<br>`
15
- assert.ok(br instanceof HElement)
16
- assert.equal(br.tag, 'br')
17
- assert.deepEqual(br.children, [])
18
- })
19
-
20
- test('static attributes', ({ assert }) => {
21
- const [el] = h`<input type="text" placeholder='name'>`
22
- assert.equal(el.attrs[0].name, 'type')
23
- assert.equal(el.attrs[0].value, 'text')
24
- assert.equal(el.attrs[1].name, 'placeholder')
25
- assert.equal(el.attrs[1].value, 'name')
26
- })
27
-
28
- test('boolean attribute (no value)', ({ assert }) => {
29
- const [el] = h`<button disabled></button>`
30
- assert.equal(el.attrs[0].name, 'disabled')
31
- assert.equal(el.attrs[0].value, undefined)
32
- })
33
-
34
- test('dynamic attribute value', ({ assert }) => {
35
- const cls = 'active'
36
- const [el] = h`<div class=${cls}></div>`
37
- assert.equal(el.attrs[0].name, 'class')
38
- assert.equal(el.attrs[0].value, 'active')
39
- })
40
-
41
- test('function slot as attribute value', ({ assert }) => {
42
- const fn = () => 'red'
43
- const [el] = h`<div style=${'color:' + fn()}></div>`
44
- assert.equal(el.attrs[0].value, 'color:red')
45
- })
46
-
47
- test('text node child', ({ assert }) => {
48
- const [el] = h`<p>hello world</p>`
49
- assert.equal(el.children.length, 1)
50
- assert.ok(el.children[0] instanceof HText)
51
- assert.equal(el.children[0].value, 'hello world')
52
- })
53
-
54
- test('dynamic child slot', ({ assert }) => {
55
- const val = 42
56
- const [el] = h`<span>${val}</span>`
57
- assert.equal(el.children[0], 42)
58
- })
59
-
60
- test('function child slot preserved as function', ({ assert }) => {
61
- const fn = () => 'dynamic'
62
- const [el] = h`<span>${fn}</span>`
63
- assert.equal(typeof el.children[0], 'function')
64
- assert.equal(el.children[0](), 'dynamic')
65
- })
66
-
67
- test('nested elements', ({ assert }) => {
68
- const [ul] = h`<ul><li>a</li><li>b</li></ul>`
69
- assert.equal(ul.tag, 'ul')
70
- assert.equal(ul.children.length, 2)
71
- assert.equal(ul.children[0].tag, 'li')
72
- assert.equal(ul.children[1].tag, 'li')
73
- })
74
-
75
- test('mixed text and element children', ({ assert }) => {
76
- const [p] = h`<p>hello <strong>world</strong></p>`
77
- assert.ok(p.children[0] instanceof HText)
78
- assert.equal(p.children[0].value, 'hello ')
79
- assert.equal(p.children[1].tag, 'strong')
80
- })
81
-
82
- test('HTML entity decoding', ({ assert }) => {
83
- const [el] = h`<p>a &amp; b &lt; c &gt; d</p>`
84
- assert.equal(el.children[0].value, 'a & b < c > d')
85
- })
86
-
87
- test('multiple root nodes', ({ assert }) => {
88
- const nodes = h`<li>a</li><li>b</li>`
89
- assert.equal(nodes.length, 2)
90
- assert.equal(nodes[0].tag, 'li')
91
- assert.equal(nodes[1].tag, 'li')
92
- })
93
-
94
- test('self-closing syntax', ({ assert }) => {
95
- const [el] = h`<MyComponent />`
96
- assert.ok(el instanceof HElement)
97
- assert.equal(el.tag, 'MyComponent')
98
- assert.deepEqual(el.children, [])
99
- })
100
-
101
- test('mixed static/dynamic attribute value parts', ({ assert }) => {
102
- // A plain string slot gets joined into the surrounding text
103
- const color = 'red'
104
- const [el1] = h`<div style="color: ${color}; font-size: 12px"></div>`
105
- assert.equal(el1.attrs[0].value, 'color: red; font-size: 12px')
106
-
107
- // A function slot stays as an array so mount() can wire up reactivity
108
- const colorFn = () => 'blue'
109
- const [el2] = h`<div style="color: ${colorFn}; font-size: 12px"></div>`
110
- assert.ok(Array.isArray(el2.attrs[0].value))
111
- assert.equal(el2.attrs[0].value[0], 'color: ')
112
- assert.equal(typeof el2.attrs[0].value[1], 'function')
113
- assert.equal(el2.attrs[0].value[2], '; font-size: 12px')
114
- })
115
-
116
- test('spread attribute object', ({ assert }) => {
117
- const extra = { id: 'foo', 'data-x': '1' }
118
- const [el] = h`<div ${extra}></div>`
119
- assert.deepEqual(el.attrs[0], extra)
120
- })
121
- })