P2-04: coalesce facade autosaves

This commit is contained in:
2026-08-02 05:21:31 -04:00
parent 5f0d9a8d0e
commit feb2eb8be6
5 changed files with 51 additions and 6 deletions

View File

@@ -1,5 +1,5 @@
export { createMockFacade } from './mockFacade'
export { createSqliteProjectPersistence, PersistenceWriteQueue, SqliteProjectPersistence } from './projectStore'
export { createSqliteProjectPersistence, PersistenceWriteQueue, ProjectAutosaveScheduler, 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, ProjectResource, ProjectSaveResult, TaskSnapshot } from './types'

View File

@@ -13,7 +13,7 @@ import type {
TaskSnapshot,
Unsubscribe,
} from './types'
import { createSqliteProjectPersistence } from './projectStore'
import { createSqliteProjectPersistence, ProjectAutosaveScheduler } from './projectStore'
import { ThreeViewportAdapter } from './threeViewport'
const initialTree: ModelTreeItem[] = [
@@ -51,6 +51,7 @@ const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedO
export function createMockFacade(): BitBybitWebCadFacade {
const projectPersistence = createSqliteProjectPersistence()
const autosave = new ProjectAutosaveScheduler((document) => projectPersistence.save(document))
let state: FacadeState = { apiVersion: '0.1', activeWorkbench: 'Part Design', selectedObjectId: 'pad', document: createDocument(), persistence: projectPersistence.capabilities(), task: null, lastNotice: '', diagnostics: [] }
const listeners = new Set<FacadeListener>()
const undoStack: FacadeState[] = []
@@ -60,7 +61,7 @@ export function createMockFacade(): BitBybitWebCadFacade {
const emit = (event: FacadeEvent) => listeners.forEach((listener) => listener(event))
const emitState = () => emit({ type: 'state.changed', state: getState() })
const getState = () => ({ ...state, diagnostics: state.diagnostics.map((diagnostic) => ({ ...diagnostic })), document: { ...state.document, tree: state.document.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })) }, task: state.task ? { ...state.task, draft: { ...state.task.draft } } : null })
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; emitState() }
const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty) autosave.schedule(next.document); emitState() }
const notify = (message: string) => { state = { ...state, lastNotice: message }; emit({ type: 'notice', message }); emitState() }
const setActive = (id: WorkbenchId) => { state = { ...state, activeWorkbench: id }; emitState(); notify(`${id} workbench loaded`) }
const select = (objectId: string) => { state = { ...state, selectedObjectId: objectId }; emitState() }

View File

@@ -35,6 +35,35 @@ export class PersistenceWriteQueue {
}
}
export class ProjectAutosaveScheduler {
private timer: ReturnType<typeof setTimeout> | null = null
private pending: DocumentSnapshot | null = null
private flushPromise: Promise<ProjectSaveResult | null> | null = null
constructor(private readonly save: (document: DocumentSnapshot) => Promise<ProjectSaveResult>, private readonly idleMs = 800) {}
schedule(document: DocumentSnapshot) {
this.pending = cloneDocument(document)
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => { this.timer = null; void this.flush() }, this.idleMs)
}
flush() {
if (this.flushPromise) return this.flushPromise
const document = this.pending
this.pending = null
if (!document) return Promise.resolve(null)
this.flushPromise = this.save(document).finally(() => { this.flushPromise = null; if (this.pending) void this.flush() })
return this.flushPromise
}
cancel() {
if (this.timer) clearTimeout(this.timer)
this.timer = null
this.pending = null
}
}
export class SqliteProjectPersistence implements ProjectPersistenceClient {
private readonly worker: Worker | null
private nextRequestId = 0