@dtudury/streamo 0.1.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/.claude/settings.local.json +10 -0
- package/LICENSE +661 -0
- package/README.md +194 -0
- package/ROADMAP.md +111 -0
- package/bin/streamo.js +238 -0
- package/jsconfig.json +9 -0
- package/package.json +26 -0
- package/public/apps/chat/index.html +61 -0
- package/public/apps/chat/main.js +144 -0
- package/public/apps/styles/proto.css +71 -0
- package/public/index.html +109 -0
- package/public/streamo/Addressifier.js +212 -0
- package/public/streamo/CodecRegistry.js +195 -0
- package/public/streamo/ContentMap.js +79 -0
- package/public/streamo/DESIGN.md +61 -0
- package/public/streamo/Repo.js +176 -0
- package/public/streamo/Repo.test.js +82 -0
- package/public/streamo/RepoRegistry.js +91 -0
- package/public/streamo/RepoRegistry.test.js +87 -0
- package/public/streamo/Signature.js +15 -0
- package/public/streamo/Signer.js +91 -0
- package/public/streamo/Streamo.js +392 -0
- package/public/streamo/Streamo.test.js +205 -0
- package/public/streamo/archiveSync.js +62 -0
- package/public/streamo/chat-cli.js +122 -0
- package/public/streamo/chat-server.js +60 -0
- package/public/streamo/codecs.js +400 -0
- package/public/streamo/fileSync.js +238 -0
- package/public/streamo/h.js +202 -0
- package/public/streamo/h.mount.test.js +67 -0
- package/public/streamo/h.test.js +121 -0
- package/public/streamo/mount.js +248 -0
- package/public/streamo/originSync.js +60 -0
- package/public/streamo/outletSync.js +105 -0
- package/public/streamo/registrySync.js +333 -0
- package/public/streamo/registrySync.test.js +373 -0
- package/public/streamo/s3Sync.js +99 -0
- package/public/streamo/stateFileSync.js +17 -0
- package/public/streamo/sync.test.js +98 -0
- package/public/streamo/utils/NestedSet.js +41 -0
- package/public/streamo/utils/Recaller.js +77 -0
- package/public/streamo/utils/mockDOM.js +113 -0
- package/public/streamo/utils/nextTick.js +22 -0
- package/public/streamo/utils/noble-secp256k1.js +602 -0
- package/public/streamo/utils/testing.js +90 -0
- package/public/streamo/utils.js +57 -0
- package/public/streamo/webSync.js +118 -0
- package/scripts/serve.js +15 -0
- package/smoke.test.js +132 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal test utility for stream/ tests.
|
|
3
|
+
*
|
|
4
|
+
* In Node.js: wraps node:test describe/test — tests actually execute via `node --test`
|
|
5
|
+
* In browser: no-op for now; browser test runner to be rebuilt as a stream module
|
|
6
|
+
*
|
|
7
|
+
* Usage in test files:
|
|
8
|
+
*
|
|
9
|
+
* import { describe, assert } from './utils/testing.js'
|
|
10
|
+
*
|
|
11
|
+
* describe(import.meta.url, ({ test }) => {
|
|
12
|
+
* test('my case', () => {
|
|
13
|
+
* assert.equal(actual, expected)
|
|
14
|
+
* })
|
|
15
|
+
* test('async case', async () => {
|
|
16
|
+
* const result = await someAsyncThing()
|
|
17
|
+
* assert.ok(result)
|
|
18
|
+
* })
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const IS_NODE = typeof process !== 'undefined' && process.versions?.node != null
|
|
23
|
+
|
|
24
|
+
// ── Assertions ────────────────────────────────────────────────────────────
|
|
25
|
+
// Throws on failure, works in any environment.
|
|
26
|
+
|
|
27
|
+
class AssertionError extends Error {
|
|
28
|
+
constructor (message) {
|
|
29
|
+
super(message)
|
|
30
|
+
this.name = 'AssertionError'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fmt (v) {
|
|
35
|
+
try {
|
|
36
|
+
if (v instanceof Uint8Array) return `Uint8Array[${[...v]}]`
|
|
37
|
+
return JSON.stringify(v)
|
|
38
|
+
} catch {
|
|
39
|
+
return String(v)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const assert = {
|
|
44
|
+
ok (val, msg) {
|
|
45
|
+
if (!val) throw new AssertionError(msg ?? `expected truthy, got ${fmt(val)}`)
|
|
46
|
+
},
|
|
47
|
+
equal (actual, expected, msg) {
|
|
48
|
+
if (actual !== expected) throw new AssertionError(msg ?? `${fmt(actual)} !== ${fmt(expected)}`)
|
|
49
|
+
},
|
|
50
|
+
notEqual (actual, expected, msg) {
|
|
51
|
+
if (actual === expected) throw new AssertionError(msg ?? `expected values to differ, both were ${fmt(actual)}`)
|
|
52
|
+
},
|
|
53
|
+
deepEqual (actual, expected, msg) {
|
|
54
|
+
if (fmt(actual) !== fmt(expected)) throw new AssertionError(msg ?? `${fmt(actual)} !== ${fmt(expected)}`)
|
|
55
|
+
},
|
|
56
|
+
throws (fn, msg) {
|
|
57
|
+
let threw = false
|
|
58
|
+
try { fn() } catch { threw = true }
|
|
59
|
+
if (!threw) throw new AssertionError(msg ?? 'expected function to throw')
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── describe / test ───────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
let _impl
|
|
66
|
+
|
|
67
|
+
if (IS_NODE) {
|
|
68
|
+
const { describe: nodeDescribe, test: nodeTest } = await import('node:test')
|
|
69
|
+
_impl = {
|
|
70
|
+
describe (name, fn) {
|
|
71
|
+
nodeDescribe(name, () => fn({
|
|
72
|
+
test: (testName, testFn) => nodeTest(testName, () => testFn({ assert }))
|
|
73
|
+
}))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
// Browser test runner: TODO rebuild as a first-class stream module
|
|
78
|
+
_impl = { describe () {} }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Declare a group of tests. Pass import.meta.url as the name so the
|
|
83
|
+
* test runner can show which file each group came from.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} name typically import.meta.url
|
|
86
|
+
* @param {function({ test: function }): void} fn
|
|
87
|
+
*/
|
|
88
|
+
export function describe (name, fn) {
|
|
89
|
+
_impl.describe(name, fn)
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encode a Uint8Array as a lowercase hex string.
|
|
3
|
+
* @param {Uint8Array} bytes
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function bytesToHex (bytes) {
|
|
7
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Decode a hex string to a Uint8Array.
|
|
12
|
+
* @param {string} hex
|
|
13
|
+
* @returns {Uint8Array}
|
|
14
|
+
*/
|
|
15
|
+
export function hexToBytes (hex) {
|
|
16
|
+
const bytes = new Uint8Array(hex.length / 2)
|
|
17
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
|
18
|
+
return bytes
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Encode a non-negative integer as a little-endian byte array.
|
|
23
|
+
* The number of bytes needed is the minimum to represent the value.
|
|
24
|
+
* @param {number} n
|
|
25
|
+
* @returns {Uint8Array}
|
|
26
|
+
*/
|
|
27
|
+
export function numberToVar (n) {
|
|
28
|
+
if (n < 0) throw new Error('n must be non-negative')
|
|
29
|
+
if (!n) return new Uint8Array([0])
|
|
30
|
+
const bytes = []
|
|
31
|
+
while (n) {
|
|
32
|
+
bytes.push(n & 0xff)
|
|
33
|
+
n >>>= 8
|
|
34
|
+
}
|
|
35
|
+
return new Uint8Array(bytes)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Decode a little-endian byte array back to a number.
|
|
40
|
+
* @param {Uint8Array} bytes
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function varToNumber (bytes) {
|
|
44
|
+
let n = 0
|
|
45
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
46
|
+
n = (n * 256 + bytes[i]) >>> 0
|
|
47
|
+
}
|
|
48
|
+
return n
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {number} length
|
|
53
|
+
* @returns {Array.<number>}
|
|
54
|
+
*/
|
|
55
|
+
export function range (length) {
|
|
56
|
+
return Array.from({ length }, (_, i) => i)
|
|
57
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createServer } from 'http'
|
|
2
|
+
import { WebSocketServer } from 'ws'
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import { fileURLToPath } from 'url'
|
|
5
|
+
import { dirname, join } from 'path'
|
|
6
|
+
import { attachStreamSync } from './outletSync.js'
|
|
7
|
+
|
|
8
|
+
const publicDir = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Start an HTTP + WebSocket server that exposes a RepoRegistry to browsers
|
|
12
|
+
* and other peers.
|
|
13
|
+
*
|
|
14
|
+
* HTTP endpoints:
|
|
15
|
+
* GET / → 200 JSON: current value of the primary streamo
|
|
16
|
+
* GET /streams/:key → 200 JSON: current value of streamo `key`
|
|
17
|
+
* GET /streams/:key/raw → 200 application/octet-stream: full wire-format archive
|
|
18
|
+
*
|
|
19
|
+
* WebSocket (same port, upgraded from HTTP):
|
|
20
|
+
* Uses the same handshake + full-duplex wire protocol as outletSync, so any
|
|
21
|
+
* originSync client can connect here directly.
|
|
22
|
+
*
|
|
23
|
+
* @param {import('./RepoRegistry.js').RepoRegistry} registry
|
|
24
|
+
* @param {string} primaryKeyHex public key of the "main" streamo for GET /
|
|
25
|
+
* @param {number} port
|
|
26
|
+
* @returns {Promise<import('http').Server>}
|
|
27
|
+
*/
|
|
28
|
+
export async function webSync (registry, primaryKeyHex, port, name, keyIterations = 100000) {
|
|
29
|
+
const app = express()
|
|
30
|
+
|
|
31
|
+
app.use(express.static(publicDir))
|
|
32
|
+
|
|
33
|
+
app.use(express.json())
|
|
34
|
+
|
|
35
|
+
// Expose primary key so the browser app knows which streamo to open
|
|
36
|
+
app.get('/api/info', (req, res) => {
|
|
37
|
+
res.json({ primaryKeyHex, name, keyIterations })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Write a single file to the primary streamo's latest commit
|
|
41
|
+
app.post('/api/file', async (req, res) => {
|
|
42
|
+
try {
|
|
43
|
+
const { path, content, message } = req.body
|
|
44
|
+
if (typeof path !== 'string' || typeof content !== 'string') {
|
|
45
|
+
return res.status(400).json({ error: 'path and content must be strings' })
|
|
46
|
+
}
|
|
47
|
+
const repo = await registry.open(primaryKeyHex)
|
|
48
|
+
const working = repo.checkout()
|
|
49
|
+
// Store JSON files as parsed objects so they round-trip cleanly with fileSync
|
|
50
|
+
let value = content
|
|
51
|
+
if (path.endsWith('.json')) {
|
|
52
|
+
try { value = JSON.parse(content) } catch {}
|
|
53
|
+
}
|
|
54
|
+
working.set(path, value)
|
|
55
|
+
repo.commit(working, message || `edit ${path}`)
|
|
56
|
+
res.json({ ok: true })
|
|
57
|
+
} catch (e) {
|
|
58
|
+
res.status(500).json({ error: e.message })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Current value of the primary streamo as JSON
|
|
63
|
+
app.get('/', async (req, res) => {
|
|
64
|
+
try {
|
|
65
|
+
const streamo = await registry.open(primaryKeyHex)
|
|
66
|
+
res.json(streamo.byteLength > 0 ? streamo.get() : null)
|
|
67
|
+
} catch (e) {
|
|
68
|
+
res.status(500).json({ error: e.message })
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Current value of any streamo as JSON
|
|
73
|
+
app.get('/streams/:key', async (req, res) => {
|
|
74
|
+
try {
|
|
75
|
+
const streamo = await registry.open(req.params.key)
|
|
76
|
+
res.json(streamo.byteLength > 0 ? streamo.get() : null)
|
|
77
|
+
} catch (e) {
|
|
78
|
+
res.status(500).json({ error: e.message })
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// Full wire-format snapshot of a streamo's current chunks (finite — does not
|
|
83
|
+
// stream future appends). Used by browsers to bootstrap before WebSocket sync
|
|
84
|
+
// so both sides share the same address space.
|
|
85
|
+
app.get('/streams/:key/raw', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const streamo = await registry.open(req.params.key)
|
|
88
|
+
res.set('Content-Type', 'application/octet-stream')
|
|
89
|
+
const target = streamo.byteLength // snapshot length; stop here
|
|
90
|
+
if (target === 0) { res.end(); return }
|
|
91
|
+
const reader = streamo.makeReadableStream().getReader()
|
|
92
|
+
res.on('close', () => reader.cancel().catch(() => {}))
|
|
93
|
+
let contentSent = 0
|
|
94
|
+
const pump = async () => {
|
|
95
|
+
if (contentSent >= target) { reader.cancel().catch(() => {}); res.end(); return }
|
|
96
|
+
const { value, done } = await reader.read()
|
|
97
|
+
if (done || !res.writable) { res.end(); return }
|
|
98
|
+
// wire frame: [4-byte LE length][chunk bytes] — read length to track progress
|
|
99
|
+
contentSent += (value[0]) | (value[1] << 8) | (value[2] << 16) | (value[3] << 24)
|
|
100
|
+
res.write(Buffer.from(value))
|
|
101
|
+
pump()
|
|
102
|
+
}
|
|
103
|
+
pump()
|
|
104
|
+
} catch (e) {
|
|
105
|
+
res.status(500).json({ error: e.message })
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const server = createServer(app)
|
|
110
|
+
|
|
111
|
+
attachStreamSync(new WebSocketServer({ server }), registry, 'web')
|
|
112
|
+
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
server.listen(port, err => err ? reject(err) : resolve())
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return server
|
|
118
|
+
}
|
package/scripts/serve.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Serve public/ as a static site.
|
|
4
|
+
* Usage: node scripts/serve.js [port]
|
|
5
|
+
*/
|
|
6
|
+
import express from 'express'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import { join, dirname } from 'path'
|
|
9
|
+
|
|
10
|
+
const port = Number(process.argv[2] ?? process.env.PORT ?? 3000)
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '../public')
|
|
12
|
+
|
|
13
|
+
const app = express()
|
|
14
|
+
app.use(express.static(root))
|
|
15
|
+
app.listen(port, () => console.log(`http://localhost:${port}`))
|
package/smoke.test.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe } from './public/streamo/utils/testing.js'
|
|
2
|
+
import { Streamo } from './public/streamo/Streamo.js'
|
|
3
|
+
import { Repo } from './public/streamo/Repo.js'
|
|
4
|
+
import { RepoRegistry } from './public/streamo/RepoRegistry.js'
|
|
5
|
+
import { archiveSync } from './public/streamo/archiveSync.js'
|
|
6
|
+
import { webSync } from './public/streamo/webSync.js'
|
|
7
|
+
import { Signer } from './public/streamo/Signer.js'
|
|
8
|
+
import WebSocket from 'ws'
|
|
9
|
+
import { rm, mkdtemp } from 'fs/promises'
|
|
10
|
+
import { tmpdir } from 'os'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
|
|
13
|
+
// 1 iteration keeps key derivation fast without compromising what we're testing
|
|
14
|
+
const KEY_ITERATIONS = 1
|
|
15
|
+
const toHex = bytes => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
16
|
+
|
|
17
|
+
async function makeKey (name = 'smoke') {
|
|
18
|
+
const signer = new Signer('test', 'test', KEY_ITERATIONS)
|
|
19
|
+
const { publicKey } = await signer.keysFor(name)
|
|
20
|
+
return { signer, publicKey, publicKeyHex: toHex(publicKey) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function startServer (publicKeyHex, stream) {
|
|
24
|
+
const registry = new RepoRegistry(() => stream)
|
|
25
|
+
const server = await webSync(registry, publicKeyHex, 0, 'smoke-test', KEY_ITERATIONS)
|
|
26
|
+
const { port } = server.address()
|
|
27
|
+
const close = () => new Promise(resolve => server.close(resolve))
|
|
28
|
+
return { port, close }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe(import.meta.url, ({ test }) => {
|
|
32
|
+
test('GET /api/info returns primaryKeyHex and name', async ({ assert }) => {
|
|
33
|
+
const { publicKeyHex } = await makeKey()
|
|
34
|
+
const { port, close } = await startServer(publicKeyHex, new Streamo())
|
|
35
|
+
try {
|
|
36
|
+
const info = await fetch(`http://localhost:${port}/api/info`).then(r => r.json())
|
|
37
|
+
assert.equal(info.primaryKeyHex, publicKeyHex)
|
|
38
|
+
assert.equal(info.name, 'smoke-test')
|
|
39
|
+
} finally {
|
|
40
|
+
await close()
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('GET /streams/:key/raw loads into a fresh Streamo', async ({ assert }) => {
|
|
45
|
+
const { publicKeyHex } = await makeKey()
|
|
46
|
+
const stream = new Streamo()
|
|
47
|
+
stream.set({ hello: 'world' })
|
|
48
|
+
const { port, close } = await startServer(publicKeyHex, stream)
|
|
49
|
+
try {
|
|
50
|
+
const buf = await fetch(`http://localhost:${port}/streams/${publicKeyHex}/raw`)
|
|
51
|
+
.then(r => r.arrayBuffer())
|
|
52
|
+
const fresh = new Streamo()
|
|
53
|
+
await fresh.makeWritableStream().getWriter().write(new Uint8Array(buf))
|
|
54
|
+
assert.deepEqual(fresh.get(), { hello: 'world' })
|
|
55
|
+
} finally {
|
|
56
|
+
await close()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('WebSocket syncs existing chunks to a connecting client', async ({ assert }) => {
|
|
61
|
+
const { publicKeyHex } = await makeKey()
|
|
62
|
+
const stream = new Streamo()
|
|
63
|
+
stream.set({ synced: true })
|
|
64
|
+
const { port, close } = await startServer(publicKeyHex, stream)
|
|
65
|
+
try {
|
|
66
|
+
const client = new Streamo()
|
|
67
|
+
const writer = client.makeWritableStream().getWriter()
|
|
68
|
+
const ws = new WebSocket(`ws://localhost:${port}`)
|
|
69
|
+
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
ws.on('open', () => ws.send(publicKeyHex))
|
|
72
|
+
ws.on('message', async data => {
|
|
73
|
+
await writer.write(new Uint8Array(data))
|
|
74
|
+
if (client.byteLength >= stream.byteLength) resolve()
|
|
75
|
+
})
|
|
76
|
+
ws.on('error', reject)
|
|
77
|
+
setTimeout(() => reject(new Error('WS sync timed out')), 2000)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
ws.close()
|
|
81
|
+
assert.deepEqual(client.get(), { synced: true })
|
|
82
|
+
} finally {
|
|
83
|
+
await close()
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('set() with a path works when root is VARIABLE-encoded (old server data)', ({ assert }) => {
|
|
88
|
+
// Old server archives encoded the root value as VARIABLE (a boxed address).
|
|
89
|
+
// asRefs() previously returned the VARIABLE address number rather than the
|
|
90
|
+
// inner object's refs, causing refs['toggle'] === undefined → crash.
|
|
91
|
+
const stream = new Streamo()
|
|
92
|
+
// encodeVariable to simulate old-style encoded root
|
|
93
|
+
const rootCode = stream.encodeVariable({ toggle: { value: false, label: 'enabled' }, counter: { value: 0 } })
|
|
94
|
+
stream.append(rootCode)
|
|
95
|
+
// get() should see through VARIABLE
|
|
96
|
+
assert.equal(stream.get('toggle', 'value'), false)
|
|
97
|
+
// set() with a 2-level path must not crash
|
|
98
|
+
stream.set('toggle', 'value', true)
|
|
99
|
+
assert.equal(stream.get('toggle', 'value'), true)
|
|
100
|
+
assert.equal(stream.get('counter', 'value'), 0)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('set() after sign() reads/writes user data, not the signature chunk', async ({ assert }) => {
|
|
104
|
+
const { signer } = await makeKey()
|
|
105
|
+
const stream = new Streamo()
|
|
106
|
+
stream.set({ count: 0 })
|
|
107
|
+
await stream.sign(signer, 'smoke')
|
|
108
|
+
// Before the fix, set() with a path would crash here because byteLength - 1
|
|
109
|
+
// pointed to the signature chunk instead of the data chunk.
|
|
110
|
+
stream.set('count', stream.get('count') + 1)
|
|
111
|
+
assert.equal(stream.get('count'), 1)
|
|
112
|
+
// get() must also skip past the signature
|
|
113
|
+
assert.deepEqual(stream.get(), { count: 1 })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('archiveSync persists data and reloads it on a fresh Streamo', async ({ assert }) => {
|
|
117
|
+
const { publicKeyHex } = await makeKey('archive')
|
|
118
|
+
const dir = await mkdtemp(join(tmpdir(), 'smoke-'))
|
|
119
|
+
try {
|
|
120
|
+
const stream1 = new Streamo()
|
|
121
|
+
await archiveSync(stream1, dir, publicKeyHex)
|
|
122
|
+
stream1.set({ persisted: true })
|
|
123
|
+
await new Promise(r => setTimeout(r, 100)) // let write loop flush
|
|
124
|
+
|
|
125
|
+
const stream2 = new Streamo()
|
|
126
|
+
await archiveSync(stream2, dir, publicKeyHex)
|
|
127
|
+
assert.deepEqual(stream2.get(), { persisted: true })
|
|
128
|
+
} finally {
|
|
129
|
+
await rm(dir, { recursive: true, force: true })
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
})
|