P2-02: serialize persistence writes
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user