mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 09:37:33 +00:00
* feat(ai-vault): read OpenCode history with the pinned Node instead of Bun SSH hosts whose Node lacks node:sqlite (or its backup(), which 22.13-22.15 omit) now get the pinned Node in the shared ~/.orca-remote/runtimes/node-<sha> store orcad uses: POSIX hosts receive the official archive and extract and hash-verify it on the host; Windows hosts receive the verified node.exe the client extracted, promoted by host Node with the same hash check. WSL distros use the same layout and checks under ~/.cache/orca/runtimes/. The Bun release pin table and its materializer are deleted. Old relays keep reading their vault-sqlite/<sha>/bun references; nothing deletes those files. An unconfirmed runtime upload now keeps its stage instead of removing it. * refactor(sqlite): drop the Bun SQLite adapter; node:sqlite is the only backend Nothing outside Electron runs on Bun any more (design D4), so SyncDatabase loses its Bun branch, and bun-sqlite-database, bun-sqlite-statement and bun-readonly-wal go, with the relay's bun:sqlite external. The profile-state backup worker admits Electron or an entry that exists, and startup errors name the pinned Node. The D7 cross-runtime gate still runs Bun 1.4.2, now reaching Bun's SQLite through its node:sqlite. * test(native-chat): drop the Bun SQLite driver case now that node:sqlite is the only backend --------- Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
85 lines
3.4 KiB
TypeScript
85 lines
3.4 KiB
TypeScript
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { Worker } from 'node:worker_threads'
|
|
import { expect, it } from 'vitest'
|
|
import SyncDatabase from './sync-database'
|
|
|
|
const WRITER_SOURCE = `
|
|
const { parentPort, workerData } = require('node:worker_threads')
|
|
const Database = require('node:sqlite').DatabaseSync
|
|
const db = new Database(workerData.path)
|
|
db.exec('PRAGMA busy_timeout=5000; PRAGMA synchronous=FULL')
|
|
const count = new Int32Array(workerData.count)
|
|
let revision = 0
|
|
function commit() {
|
|
revision += 1
|
|
db.exec('BEGIN IMMEDIATE; UPDATE marker SET revision=' + revision + '; COMMIT')
|
|
Atomics.store(count, 0, revision)
|
|
if (revision === 1) {
|
|
parentPort.once('message', commit)
|
|
parentPort.postMessage('writing')
|
|
return
|
|
}
|
|
if (revision < 40) setTimeout(commit, 2)
|
|
else { db.close(true); parentPort.close() }
|
|
}
|
|
commit()
|
|
`
|
|
|
|
it('backs up one complete revision while another thread commits to the WAL', async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'orca-sqlite-concurrent-backup-'))
|
|
const path = join(directory, 'source.db')
|
|
const target = join(directory, 'snapshot.db')
|
|
const source = new SyncDatabase(path)
|
|
let writer: Worker | undefined
|
|
let snapshot: SyncDatabase | undefined
|
|
try {
|
|
source.exec(`
|
|
PRAGMA journal_mode=WAL;
|
|
PRAGMA synchronous=FULL;
|
|
CREATE TABLE marker(name TEXT PRIMARY KEY, revision INTEGER NOT NULL);
|
|
INSERT INTO marker VALUES('first',0),('second',0);
|
|
CREATE TABLE payload(id INTEGER PRIMARY KEY, value BLOB NOT NULL);
|
|
WITH RECURSIVE rows(id) AS (VALUES(1) UNION ALL SELECT id+1 FROM rows WHERE id<1024)
|
|
INSERT INTO payload SELECT id, zeroblob(65536) FROM rows;
|
|
`)
|
|
const sharedCount = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)
|
|
const count = new Int32Array(sharedCount)
|
|
writer = new Worker(WRITER_SOURCE, { eval: true, workerData: { path, count: sharedCount } })
|
|
const firstCommit = new Promise<void>((resolve, reject) => {
|
|
writer?.once('message', () => resolve())
|
|
writer?.once('error', reject)
|
|
})
|
|
const writerExit = new Promise<void>((resolve, reject) => {
|
|
writer?.once('error', reject)
|
|
writer?.once('exit', (code) =>
|
|
code === 0 ? resolve() : reject(new Error(`Writer exited ${code}`))
|
|
)
|
|
})
|
|
// Attach failure handling before awaiting either event.
|
|
void writerExit.catch(() => {})
|
|
await firstCommit
|
|
const before = Atomics.load(count, 0)
|
|
// Keep the writer alive until this thread is ready to start the backup.
|
|
writer.postMessage('continue')
|
|
await source.backup(target)
|
|
const after = Atomics.load(count, 0)
|
|
expect(after).toBeGreaterThan(before)
|
|
await writerExit
|
|
snapshot = new SyncDatabase(target, { readonly: true, fileMustExist: true })
|
|
const rows = snapshot.prepare('SELECT revision FROM marker ORDER BY name').all()
|
|
expect(rows).toHaveLength(2)
|
|
expect(rows[0]).toEqual(rows[1])
|
|
expect(rows[0]?.revision).toBeGreaterThanOrEqual(before)
|
|
expect(rows[0]?.revision).toBeLessThanOrEqual(Atomics.load(count, 0))
|
|
expect(snapshot.prepare('SELECT count(*) AS count FROM payload').get()).toEqual({ count: 1024 })
|
|
expect(snapshot.pragma('integrity_check', { simple: true })).toBe('ok')
|
|
} finally {
|
|
await writer?.terminate()
|
|
snapshot?.close()
|
|
source.close()
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
}, 20_000)
|