P2-02: serialize persistence writes

This commit is contained in:
2026-08-02 04:58:52 -04:00
parent cc7cfc6153
commit 657c4beb9f
4 changed files with 53 additions and 10 deletions

View File

@@ -1,5 +1,5 @@
export { createMockFacade } from './mockFacade'
export { createSqliteProjectPersistence, SqliteProjectPersistence } from './projectStore'
export { createSqliteProjectPersistence, PersistenceWriteQueue, SqliteProjectPersistence } from './projectStore'
export { ThreeViewportAdapter } from './threeViewport'
export { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_SQL, PROJECT_SCHEMA_VERSION } from './projectSchema'
export type { BitBybitWebCadFacade, CommandState, DocumentSnapshot, FacadeEvent, FacadeState, ModelTreeItem, PersistenceCapabilities, ProjectSaveResult, TaskSnapshot } from './types'

View File

@@ -15,6 +15,21 @@ export interface ProjectPersistenceClient {
dispose(): Promise<void>
}
/** Serializes persistence mutations while allowing reads to follow the write tail. */
export class PersistenceWriteQueue {
private tail: Promise<void> = Promise.resolve()
run<T>(operation: () => Promise<T>): Promise<T> {
const next = this.tail.then(operation)
this.tail = next.then(() => undefined, () => undefined)
return next
}
drain(): Promise<void> {
return this.tail
}
}
export class SqliteProjectPersistence implements ProjectPersistenceClient {
private readonly worker: Worker | null
private nextRequestId = 0
@@ -22,6 +37,7 @@ export class SqliteProjectPersistence implements ProjectPersistenceClient {
private initialized: Promise<PersistenceCapabilities> | null = null
private readonly fallbackSnapshots = new Map<string, DocumentSnapshot>()
private readonly pending = new Map<number, { resolve: (response: WorkerResponse) => void; reject: (error: Error) => void }>()
private readonly writeQueue = new PersistenceWriteQueue()
constructor() {
this.worker = typeof Worker === 'undefined' ? null : new Worker(new URL('./persistenceWorker.ts', import.meta.url), { type: 'module', name: 'bitbybit-persistence' })
@@ -40,15 +56,18 @@ export class SqliteProjectPersistence implements ProjectPersistenceClient {
return this.initialized
}
async save(document: DocumentSnapshot) {
await this.initialize()
if (!this.worker) { const snapshot = cloneDocument(document); this.fallbackSnapshots.set(snapshot.id, snapshot); return { documentId: snapshot.id, documentVersion: snapshot.version, persistedAt: Date.now(), mode: this.currentCapabilities.mode } }
const response = await this.request({ type: 'save-document', document })
if (!response.ok || response.type !== 'saved') throw new Error(response.ok ? 'Unexpected persistence response.' : response.error)
return { documentId: response.documentId, documentVersion: response.documentVersion, persistedAt: response.persistedAt, mode: response.mode }
save(document: DocumentSnapshot) {
return this.writeQueue.run(async () => {
await this.initialize()
if (!this.worker) { const snapshot = cloneDocument(document); this.fallbackSnapshots.set(snapshot.id, snapshot); return { documentId: snapshot.id, documentVersion: snapshot.version, persistedAt: Date.now(), mode: this.currentCapabilities.mode } }
const response = await this.request({ type: 'save-document', document })
if (!response.ok || response.type !== 'saved') throw new Error(response.ok ? 'Unexpected persistence response.' : response.error)
return { documentId: response.documentId, documentVersion: response.documentVersion, persistedAt: response.persistedAt, mode: response.mode }
})
}
async load(documentId: string) {
await this.writeQueue.drain()
await this.initialize()
if (!this.worker) { const snapshot = this.fallbackSnapshots.get(documentId); return snapshot ? cloneDocument(snapshot) : null }
const response = await this.request({ type: 'load-document', documentId })
@@ -57,6 +76,7 @@ export class SqliteProjectPersistence implements ProjectPersistenceClient {
}
async dispose() {
await this.writeQueue.drain()
if (!this.worker) return
await this.request({ type: 'dispose' })
this.worker.terminate()