P2-01: add SQLite WASM OPFS project worker
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
export { createMockFacade } from './mockFacade'
|
||||
export { createSqliteProjectPersistence, SqliteProjectPersistence } from './projectStore'
|
||||
export { ThreeViewportAdapter } from './threeViewport'
|
||||
export type { BitBybitWebCadFacade, CommandState, DocumentSnapshot, FacadeEvent, FacadeState, ModelTreeItem, TaskSnapshot } from './types'
|
||||
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'
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
TaskSnapshot,
|
||||
Unsubscribe,
|
||||
} from './types'
|
||||
import { createSqliteProjectPersistence } from './projectStore'
|
||||
import { ThreeViewportAdapter } from './threeViewport'
|
||||
|
||||
const initialTree: ModelTreeItem[] = [
|
||||
@@ -49,7 +50,8 @@ const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedO
|
||||
}
|
||||
|
||||
export function createMockFacade(): BitBybitWebCadFacade {
|
||||
let state: FacadeState = { apiVersion: '0.1', activeWorkbench: 'Part Design', selectedObjectId: 'pad', document: createDocument(), task: null, lastNotice: '', diagnostics: [] }
|
||||
const projectPersistence = createSqliteProjectPersistence()
|
||||
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[] = []
|
||||
const redoStack: FacadeState[] = []
|
||||
@@ -106,7 +108,15 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
}
|
||||
emit({ type: 'command.started', commandId, context })
|
||||
if (commandId === 'new-document') commit({ ...state, document: createDocument('Untitled document'), selectedObjectId: '' })
|
||||
else if (commandId === 'save') commit({ ...state, document: { ...state.document, dirty: false, version: state.document.version + 1 } })
|
||||
else if (commandId === 'save') {
|
||||
const savedDocument = { ...state.document, dirty: false, version: state.document.version + 1 }
|
||||
commit({ ...state, document: savedDocument })
|
||||
void projectPersistence.save(savedDocument).catch((error: unknown) => {
|
||||
const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, severity: 'error', code: 'PERSISTENCE_SAVE_FAILED', message: error instanceof Error ? error.message : String(error), requestId }
|
||||
state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
|
||||
emit({ type: 'diagnostic.added', diagnostic, context }); notify('Save failed; export a recovery package')
|
||||
})
|
||||
}
|
||||
else if (commandId === 'select-object' && typeof payload?.objectId === 'string') select(payload.objectId)
|
||||
else if (featureCommands[commandId]) beginTask(commandId, { source: state.selectedObjectId || null })
|
||||
emit({ type: 'command.completed', commandId, context }); emitState(); return requestId
|
||||
@@ -118,8 +128,10 @@ export function createMockFacade(): BitBybitWebCadFacade {
|
||||
gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
|
||||
selection: { getObjectId: () => state.selectedObjectId, select, clear: () => select('') },
|
||||
task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
|
||||
project: { capabilities: () => projectPersistence.capabilities(), save: (document = getState().document) => projectPersistence.save(document), load: (documentId) => projectPersistence.load(documentId) },
|
||||
viewport: { createAdapter: () => new ThreeViewportAdapter() },
|
||||
getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
|
||||
}
|
||||
void projectPersistence.initialize().then((nextCapabilities) => { state = { ...state, persistence: nextCapabilities }; emitState() }).catch((error: unknown) => { const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, severity: 'warning', code: 'PERSISTENCE_INIT_FAILED', message: error instanceof Error ? error.message : String(error) }; state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }; emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: `req-${++requestSequence}`, documentId: state.document.id, documentVersion: state.document.version, workbench: state.activeWorkbench } }) })
|
||||
return facade
|
||||
}
|
||||
|
||||
107
src/facade/persistenceWorker.ts
Normal file
107
src/facade/persistenceWorker.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import sqlite3InitModule, { type Database, type Sqlite3Static } from '@sqlite.org/sqlite-wasm'
|
||||
import { PROJECT_SCHEMA_MIGRATIONS, PROJECT_SCHEMA_VERSION } from './projectSchema'
|
||||
import type { DocumentSnapshot, ModelTreeItem, PersistenceCapabilities } from './types'
|
||||
|
||||
type PersistenceRequest =
|
||||
| { id: number; type: 'initialize' }
|
||||
| { id: number; type: 'save-document'; document: DocumentSnapshot }
|
||||
| { id: number; type: 'load-document'; documentId: string }
|
||||
| { id: number; type: 'dispose' }
|
||||
|
||||
type PersistenceResponse =
|
||||
| { id: number; ok: true; type: 'initialized'; capabilities: PersistenceCapabilities }
|
||||
| { id: number; ok: true; type: 'saved'; documentId: string; documentVersion: number; persistedAt: number; mode: PersistenceCapabilities['mode'] }
|
||||
| { id: number; ok: true; type: 'loaded'; document: DocumentSnapshot | null }
|
||||
| { id: number; ok: true; type: 'disposed' }
|
||||
| { id: number; ok: false; error: string }
|
||||
|
||||
type WorkerScope = {
|
||||
postMessage(message: PersistenceResponse): void
|
||||
onmessage: ((event: MessageEvent<PersistenceRequest>) => void) | null
|
||||
}
|
||||
|
||||
const workerScope = globalThis as unknown as WorkerScope
|
||||
let sqlite3: Sqlite3Static | undefined
|
||||
let database: Database | undefined
|
||||
let capabilities: PersistenceCapabilities = { mode: 'unavailable', sqliteWasm: false, opfs: false, schemaVersion: PROJECT_SCHEMA_VERSION, reason: 'SQLite WASM has not been initialized.' }
|
||||
|
||||
const cloneTree = (tree: ModelTreeItem[]) => tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
|
||||
|
||||
const initialize = async (): Promise<PersistenceCapabilities> => {
|
||||
if (database) return capabilities
|
||||
sqlite3 = await sqlite3InitModule()
|
||||
const opfsAvailable = Boolean(sqlite3.oo1.OpfsDb)
|
||||
try {
|
||||
database = opfsAvailable ? new sqlite3.oo1.OpfsDb('/bitbybit-project.sqlite3') : new sqlite3.oo1.DB(':memory:', 'c')
|
||||
capabilities = { mode: opfsAvailable ? 'sqlite-opfs' : 'sqlite-memory', sqliteWasm: true, opfs: opfsAvailable, schemaVersion: PROJECT_SCHEMA_VERSION, reason: opfsAvailable ? undefined : 'OPFS VFS is unavailable; persistence is transient until export.' }
|
||||
} catch (error) {
|
||||
database = new sqlite3.oo1.DB(':memory:', 'c')
|
||||
capabilities = { mode: 'sqlite-memory', sqliteWasm: true, opfs: false, schemaVersion: PROJECT_SCHEMA_VERSION, reason: error instanceof Error ? `OPFS initialization failed: ${error.message}` : 'OPFS initialization failed.' }
|
||||
}
|
||||
database.exec('PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS schema_migrations (version INTEGER PRIMARY KEY, applied_at INTEGER NOT NULL);')
|
||||
database.exec('BEGIN;')
|
||||
try {
|
||||
for (const migration of PROJECT_SCHEMA_MIGRATIONS) {
|
||||
const applied = database.exec({ sql: 'SELECT version FROM schema_migrations WHERE version = ?', bind: [migration.version], returnValue: 'resultRows' }) as unknown[]
|
||||
if (applied.length === 0) {
|
||||
database.exec(migration.sql)
|
||||
database.exec({ sql: 'INSERT INTO schema_migrations(version, applied_at) VALUES(?, ?)', bind: [migration.version, Date.now()] })
|
||||
}
|
||||
}
|
||||
database.exec('COMMIT;')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK;')
|
||||
throw error
|
||||
}
|
||||
return capabilities
|
||||
}
|
||||
|
||||
const saveDocument = (document: DocumentSnapshot) => {
|
||||
if (!database) throw new Error('Persistence database is not initialized.')
|
||||
const now = Date.now()
|
||||
database.exec('BEGIN;')
|
||||
try {
|
||||
database.exec({ sql: 'INSERT INTO projects(id, name, schema_version, created_at, updated_at) VALUES(?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name, schema_version=excluded.schema_version, updated_at=excluded.updated_at', bind: [document.id, document.label, PROJECT_SCHEMA_VERSION, now, now] })
|
||||
database.exec({ sql: 'INSERT INTO documents(id, project_id, label, version, dirty, read_only, units) VALUES(?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET label=excluded.label, version=excluded.version, dirty=excluded.dirty, read_only=excluded.read_only, units=excluded.units', bind: [document.id, document.id, document.label, document.version, document.dirty ? 1 : 0, document.readOnly ? 1 : 0, document.units] })
|
||||
database.exec({ sql: 'DELETE FROM objects WHERE document_id = ?', bind: [document.id] })
|
||||
const parentByChild = new Map<string, string>()
|
||||
for (const item of document.tree) for (const childId of item.children || []) parentByChild.set(childId, item.id)
|
||||
document.tree.forEach((item, ordinal) => database?.exec({ sql: 'INSERT INTO objects(id, document_id, parent_id, label, object_type, state, detail, children_json, ordinal) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)', bind: [item.id, document.id, parentByChild.get(item.id) || null, item.label, item.type, item.state || null, item.detail || null, JSON.stringify(item.children || []), ordinal] }))
|
||||
database.exec('COMMIT;')
|
||||
} catch (error) {
|
||||
database.exec('ROLLBACK;')
|
||||
throw error
|
||||
}
|
||||
return { documentId: document.id, documentVersion: document.version, persistedAt: now, mode: capabilities.mode }
|
||||
}
|
||||
|
||||
const loadDocument = (documentId: string): DocumentSnapshot | null => {
|
||||
if (!database) throw new Error('Persistence database is not initialized.')
|
||||
const documents = database.exec({ sql: 'SELECT id, label, version, dirty, read_only, units FROM documents WHERE id = ?', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number>>
|
||||
const row = documents[0]
|
||||
if (!row) return null
|
||||
const objects = database.exec({ sql: 'SELECT id, label, object_type, state, detail, children_json FROM objects WHERE document_id = ? ORDER BY ordinal', bind: [documentId], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number | null>>
|
||||
return {
|
||||
id: String(row.id),
|
||||
label: String(row.label),
|
||||
version: Number(row.version),
|
||||
dirty: Boolean(row.dirty),
|
||||
readOnly: Boolean(row.read_only),
|
||||
units: String(row.units),
|
||||
tree: objects.map((object) => ({ id: String(object.id), label: String(object.label), type: String(object.object_type) as ModelTreeItem['type'], state: object.state ? String(object.state) as ModelTreeItem['state'] : undefined, detail: object.detail ? String(object.detail) : undefined, children: object.children_json ? JSON.parse(String(object.children_json)) as string[] : undefined })),
|
||||
}
|
||||
}
|
||||
|
||||
const handle = async (request: PersistenceRequest): Promise<PersistenceResponse> => {
|
||||
try {
|
||||
if (request.type === 'initialize') return { id: request.id, ok: true, type: 'initialized', capabilities: await initialize() }
|
||||
if (request.type === 'dispose') { database?.close(); database = undefined; return { id: request.id, ok: true, type: 'disposed' } }
|
||||
await initialize()
|
||||
if (request.type === 'save-document') { const result = saveDocument(request.document); return { id: request.id, ok: true, type: 'saved', ...result } }
|
||||
return { id: request.id, ok: true, type: 'loaded', document: loadDocument(request.documentId) }
|
||||
} catch (error) {
|
||||
return { id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
|
||||
workerScope.onmessage = (event) => { void handle(event.data).then((response) => workerScope.postMessage(response)) }
|
||||
87
src/facade/projectSchema.ts
Normal file
87
src/facade/projectSchema.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
export const PROJECT_SCHEMA_VERSION = 1
|
||||
|
||||
export const PROJECT_SCHEMA_SQL = `
|
||||
PRAGMA foreign_keys = ON;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
schema_version INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
label TEXT NOT NULL,
|
||||
version INTEGER NOT NULL,
|
||||
dirty INTEGER NOT NULL DEFAULT 0,
|
||||
read_only INTEGER NOT NULL DEFAULT 0,
|
||||
units TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS objects (
|
||||
id TEXT NOT NULL,
|
||||
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
parent_id TEXT,
|
||||
label TEXT NOT NULL,
|
||||
object_type TEXT NOT NULL,
|
||||
state TEXT,
|
||||
detail TEXT,
|
||||
children_json TEXT NOT NULL DEFAULT '[]',
|
||||
ordinal INTEGER NOT NULL,
|
||||
PRIMARY KEY (document_id, id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS objects_document_ordinal
|
||||
ON objects(document_id, ordinal);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS object_properties (
|
||||
document_id TEXT NOT NULL,
|
||||
object_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
value_json TEXT NOT NULL,
|
||||
property_type TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (document_id, object_id, name),
|
||||
FOREIGN KEY (document_id, object_id) REFERENCES objects(document_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dependencies (
|
||||
document_id TEXT NOT NULL,
|
||||
source_id TEXT NOT NULL,
|
||||
target_id TEXT NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
PRIMARY KEY (document_id, source_id, target_id, relation),
|
||||
FOREIGN KEY (document_id, source_id) REFERENCES objects(document_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (document_id, target_id) REFERENCES objects(document_id, id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
document_id TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
|
||||
version INTEGER NOT NULL,
|
||||
command_id TEXT NOT NULL,
|
||||
request_id TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS resources (
|
||||
hash TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL,
|
||||
byte_length INTEGER NOT NULL,
|
||||
media_type TEXT NOT NULL,
|
||||
ref_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`
|
||||
|
||||
export const PROJECT_SCHEMA_MIGRATIONS = [{ version: PROJECT_SCHEMA_VERSION, sql: PROJECT_SCHEMA_SQL }] as const
|
||||
75
src/facade/projectStore.ts
Normal file
75
src/facade/projectStore.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { DocumentSnapshot, PersistenceCapabilities, ProjectSaveResult } from './types'
|
||||
|
||||
type WorkerRequest = { id: number; type: 'initialize' | 'dispose' } | { id: number; type: 'save-document'; document: DocumentSnapshot } | { id: number; type: 'load-document'; documentId: string }
|
||||
type WorkerInput = { type: 'initialize' | 'dispose' } | { type: 'save-document'; document: DocumentSnapshot } | { type: 'load-document'; documentId: string }
|
||||
type WorkerResponse = { id: number; ok: true; type: 'initialized'; capabilities: PersistenceCapabilities } | { id: number; ok: true; type: 'saved'; documentId: string; documentVersion: number; persistedAt: number; mode: PersistenceCapabilities['mode'] } | { id: number; ok: true; type: 'loaded'; document: DocumentSnapshot | null } | { id: number; ok: true; type: 'disposed' } | { id: number; ok: false; error: string }
|
||||
|
||||
const unavailable: PersistenceCapabilities = { mode: 'unavailable', sqliteWasm: false, opfs: false, schemaVersion: 0, reason: 'Persistence Worker is unavailable in this environment.' }
|
||||
const cloneDocument = (document: DocumentSnapshot): DocumentSnapshot => ({ ...document, tree: document.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })) })
|
||||
|
||||
export interface ProjectPersistenceClient {
|
||||
initialize(): Promise<PersistenceCapabilities>
|
||||
capabilities(): PersistenceCapabilities
|
||||
save(document: DocumentSnapshot): Promise<ProjectSaveResult>
|
||||
load(documentId: string): Promise<DocumentSnapshot | null>
|
||||
dispose(): Promise<void>
|
||||
}
|
||||
|
||||
export class SqliteProjectPersistence implements ProjectPersistenceClient {
|
||||
private readonly worker: Worker | null
|
||||
private nextRequestId = 0
|
||||
private currentCapabilities = unavailable
|
||||
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 }>()
|
||||
|
||||
constructor() {
|
||||
this.worker = typeof Worker === 'undefined' ? null : new Worker(new URL('./persistenceWorker.ts', import.meta.url), { type: 'module', name: 'bitbybit-persistence' })
|
||||
if (!this.worker) this.currentCapabilities = { mode: 'sqlite-memory', sqliteWasm: false, opfs: false, schemaVersion: 0, reason: 'Worker is unavailable; using a transient in-memory project store.' }
|
||||
if (this.worker) {
|
||||
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => { const request = this.pending.get(event.data.id); if (!request) return; this.pending.delete(event.data.id); request.resolve(event.data) }
|
||||
this.worker.onerror = (event) => { const error = new Error(event.message || 'Persistence Worker failed.'); this.currentCapabilities = { ...unavailable, reason: error.message }; this.initialized = null; for (const request of this.pending.values()) request.reject(error); this.pending.clear() }
|
||||
}
|
||||
}
|
||||
|
||||
capabilities() { return { ...this.currentCapabilities } }
|
||||
|
||||
initialize() {
|
||||
if (!this.worker) return Promise.resolve(this.currentCapabilities)
|
||||
if (!this.initialized) this.initialized = this.request({ type: 'initialize' }).then((response) => { if (!response.ok || response.type !== 'initialized') throw new Error(response.ok ? 'Unexpected persistence response.' : response.error); this.currentCapabilities = response.capabilities; return this.capabilities() })
|
||||
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 }
|
||||
}
|
||||
|
||||
async load(documentId: string) {
|
||||
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 })
|
||||
if (!response.ok || response.type !== 'loaded') throw new Error(response.ok ? 'Unexpected persistence response.' : response.error)
|
||||
return response.document
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
if (!this.worker) return
|
||||
await this.request({ type: 'dispose' })
|
||||
this.worker.terminate()
|
||||
}
|
||||
|
||||
private request(input: WorkerInput) {
|
||||
if (!this.worker) return Promise.reject<WorkerResponse>(new Error(this.currentCapabilities.reason))
|
||||
const id = ++this.nextRequestId
|
||||
return new Promise<WorkerResponse>((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject })
|
||||
this.worker?.postMessage({ ...input, id } satisfies WorkerRequest)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const createSqliteProjectPersistence = () => new SqliteProjectPersistence()
|
||||
@@ -19,6 +19,21 @@ export type DocumentSnapshot = {
|
||||
tree: ModelTreeItem[]
|
||||
}
|
||||
|
||||
export type PersistenceCapabilities = {
|
||||
mode: 'sqlite-opfs' | 'sqlite-memory' | 'unavailable'
|
||||
sqliteWasm: boolean
|
||||
opfs: boolean
|
||||
schemaVersion: number
|
||||
reason?: string
|
||||
}
|
||||
|
||||
export type ProjectSaveResult = {
|
||||
documentId: string
|
||||
documentVersion: number
|
||||
persistedAt: number
|
||||
mode: PersistenceCapabilities['mode']
|
||||
}
|
||||
|
||||
export type CommandState = {
|
||||
id: string
|
||||
status: 'hidden' | 'disabled' | 'enabled' | 'active'
|
||||
@@ -55,6 +70,7 @@ export type FacadeState = {
|
||||
activeWorkbench: WorkbenchId
|
||||
selectedObjectId: string
|
||||
document: DocumentSnapshot
|
||||
persistence: PersistenceCapabilities
|
||||
task: TaskSnapshot | null
|
||||
lastNotice: string
|
||||
diagnostics: Diagnostic[]
|
||||
@@ -122,6 +138,11 @@ export interface BitBybitWebCadFacade {
|
||||
apply(): void
|
||||
cancel(): void
|
||||
}
|
||||
readonly project: {
|
||||
capabilities(): PersistenceCapabilities
|
||||
save(document?: DocumentSnapshot): Promise<ProjectSaveResult>
|
||||
load(documentId: string): Promise<DocumentSnapshot | null>
|
||||
}
|
||||
readonly viewport: {
|
||||
createAdapter(): BitBybitViewportAdapter
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user