P1b: SQLite project, content-addressed assets, autosave and undo
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env node
|
||||
// Restart the daemon and verify the core, pattern and tempo survived.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const PORT = 4318;
|
||||
const DAEMON = './target/debug/zhila-daemon';
|
||||
const project = mkdtempSync(join(tmpdir(), 'zhila-persist-'));
|
||||
const fixture = readFileSync('fixtures/clicks-01.wav');
|
||||
let child = null;
|
||||
|
||||
function start() {
|
||||
return new Promise((resolve, reject) => {
|
||||
child = spawn(DAEMON, ['--port', String(PORT), '--project', project, '--no-audio'], {
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
});
|
||||
child.stderr.on('data', (data) => process.stderr.write(data));
|
||||
child.on('exit', (code) => { if (code && code !== 0) console.error('daemon exited', code); });
|
||||
const attempt = () => {
|
||||
const socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
||||
socket.onopen = () => { socket.close(); resolve(); };
|
||||
socket.onerror = () => setTimeout(attempt, 150);
|
||||
};
|
||||
setTimeout(attempt, 200);
|
||||
setTimeout(() => reject(new Error('daemon did not start')), 10_000);
|
||||
});
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
const pending = new Map();
|
||||
let snapshot = null;
|
||||
let imported = null;
|
||||
const waiters = [];
|
||||
const waitFor = (predicate) => new Promise((resolve) => waiters.push({ predicate, resolve }));
|
||||
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: 'dev', client: 'persistence-smoke' }));
|
||||
ws.onmessage = (message) => {
|
||||
if (typeof message.data !== 'string') return;
|
||||
const parsed = JSON.parse(message.data);
|
||||
if (parsed.type === 'snapshot') {
|
||||
snapshot = parsed.state;
|
||||
for (const waiter of [...waiters]) if (waiter.predicate(parsed)) waiters.splice(waiters.indexOf(waiter), 1), waiter.resolve(parsed);
|
||||
} else if (parsed.type === 'imported') {
|
||||
imported = parsed;
|
||||
for (const waiter of [...waiters]) if (waiter.predicate(parsed)) waiters.splice(waiters.indexOf(waiter), 1), waiter.resolve(parsed);
|
||||
} else if (parsed.type === 'result') {
|
||||
const entry = pending.get(parsed.id);
|
||||
if (entry) { pending.delete(parsed.id); if (parsed.result.state) snapshot = parsed.result.state; entry.resolve(parsed.result); }
|
||||
} else if (parsed.type === 'error') {
|
||||
const entry = parsed.id != null ? pending.get(parsed.id) : null;
|
||||
if (entry) { pending.delete(parsed.id); entry.reject(new Error(parsed.message)); }
|
||||
}
|
||||
};
|
||||
await waitFor((message) => message.type === 'snapshot');
|
||||
const command = (id, command) => new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
ws.send(JSON.stringify({ type: 'command', id, command }));
|
||||
});
|
||||
const importFixture = async () => {
|
||||
ws.send(JSON.stringify({ type: 'import_core', id: 10, name: 'clicks-01.wav', seal: 'found' }));
|
||||
ws.send(fixture);
|
||||
await waitFor((message) => message.type === 'imported');
|
||||
return imported.core;
|
||||
};
|
||||
return { ws, command, importFixture, get snapshot() { return snapshot; } };
|
||||
}
|
||||
|
||||
try {
|
||||
await start();
|
||||
let client = await connect();
|
||||
const first = client.snapshot;
|
||||
if (!first.project_name) throw new Error('no project name in the snapshot');
|
||||
const core = await client.importFixture();
|
||||
if (core.faces.length !== 16) throw new Error('no sixteen faces');
|
||||
const top = core.faces.find((face) => face.rank === 0);
|
||||
await client.command(20, { type: 'set_pattern_face', row: 0, core: core.id, face: top.index });
|
||||
await client.command(21, { type: 'undo' });
|
||||
if (client.snapshot.pattern.rows[0].face) throw new Error('undo did not revert the face');
|
||||
await client.command(22, { type: 'redo' });
|
||||
const assigned = client.snapshot.pattern.rows[0].face;
|
||||
if (!assigned) throw new Error('redo did not restore the face');
|
||||
await client.command(23, { type: 'set_pattern_step', row: 0, step: 0, velocity: 96 });
|
||||
await client.command(24, { type: 'set_tempo', bpm: 132 });
|
||||
client.ws.close();
|
||||
await new Promise((resolve) => { child.once('exit', resolve); child.kill('SIGTERM'); });
|
||||
|
||||
await start();
|
||||
client = await connect();
|
||||
const second = client.snapshot;
|
||||
const restored = second.pattern.rows[0];
|
||||
if (second.cores.length !== 1) throw new Error('core did not survive the restart');
|
||||
if (second.cores[0].name !== 'clicks-01') throw new Error('core name changed');
|
||||
if (!restored.face || restored.face.face !== assigned.face) throw new Error('pattern face did not survive');
|
||||
if (restored.velocities[0] !== 96) throw new Error('step did not survive');
|
||||
if (second.transport.bpm !== 132) throw new Error('tempo did not survive');
|
||||
if (second.project_name !== first.project_name) throw new Error('project name changed');
|
||||
console.log(JSON.stringify({
|
||||
project: second.project_name,
|
||||
path: project,
|
||||
cores: second.cores.length,
|
||||
faces: second.cores[0].faces.length,
|
||||
face: restored.face,
|
||||
step: restored.velocities[0],
|
||||
bpm: second.transport.bpm,
|
||||
journal: second.journal_len,
|
||||
db: existsSync(join(project, 'project.db'))
|
||||
}, null, 2));
|
||||
client.ws.close();
|
||||
child.kill('SIGTERM');
|
||||
rmSync(project, { recursive: true, force: true });
|
||||
console.log('persistence smoke passed');
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
if (child) child.kill('SIGTERM');
|
||||
rmSync(project, { recursive: true, force: true });
|
||||
console.error('FAIL', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user