fix: support worker memory resource fallback

This commit is contained in:
2026-08-02 20:54:48 -04:00
parent 4be5f521ff
commit 9cc5e7847e
3 changed files with 21 additions and 1 deletions

View File

@@ -331,6 +331,7 @@ Geometry Facade 还接入了 Bitbybit IO 的 STEP 和 ASCII STL 导出,作为
| FC-03 ZIP 安全读取 | 先读中央目录再解压;限制 archive/entry/total 字节、压缩比、条目数拒绝路径穿越、重复路径、加密项、ZIP64 和 XML entity | 缺 Document.xml、路径穿越、压缩炸弹测试通过 | 尚未恢复 BRep/Shape、视图、表达式和参数化对象 | | FC-03 ZIP 安全读取 | 先读中央目录再解压;限制 archive/entry/total 字节、压缩比、条目数拒绝路径穿越、重复路径、加密项、ZIP64 和 XML entity | 缺 Document.xml、路径穿越、压缩炸弹测试通过 | 尚未恢复 BRep/Shape、视图、表达式和参数化对象 |
| FC-04/FC-07 只读报告 | `project.fcstd.inspect()` 解析 Document.xml 对象声明、Label、属性数量和类型未知类型为 proxyPython 类型为 blocked脚本资源只告警且不执行 | recognized/proxy/blocked 分类和 Facade-only 测试通过 | `metadata-compatible` 仅表示 XML 元数据可读,不表示 FCStd 可编辑或 round-trip | | FC-04/FC-07 只读报告 | `project.fcstd.inspect()` 解析 Document.xml 对象声明、Label、属性数量和类型未知类型为 proxyPython 类型为 blocked脚本资源只告警且不执行 | recognized/proxy/blocked 分类和 Facade-only 测试通过 | `metadata-compatible` 仅表示 XML 元数据可读,不表示 FCStd 可编辑或 round-trip |
| P2-04 Document load | `app.document.load()` 将 Project 快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态并保留 Undo 边界Projects 页面已调用该 Facade API | 保存→修改→加载后属性恢复、选择清空、任务清空测试通过 | FCStd 导入仍是只读 metadata/proxy跨文档 ShapeHandle 和恢复后自动几何重算仍待完成 | | P2-04 Document load | `app.document.load()` 将 Project 快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态并保留 Undo 边界Projects 页面已调用该 Facade API | 保存→修改→加载后属性恢复、选择清空、任务清空测试通过 | FCStd 导入仍是只读 metadata/proxy跨文档 ShapeHandle 和恢复后自动几何重算仍待完成 |
| P2-03 Resource fallback | SQLite Worker 在无 OPFS 的浏览器中使用内存资源表,保留 SHA-256 身份和引用计数;销毁 Worker 时清空临时资源 | Worker 类型检查、生产构建通过 | 内存资源会随刷新丢失;配额回收、持久资源校验和大文件压力仍待实现 |
| PART-07 Shape check | `check-shape` 使用 Facade `geometry.topology()` 生成面/边/顶点数量和 `SHAPE_CHECK_PASSED` 信息诊断;空缓存、空拓扑和 Worker 错误分别返回结构化失败诊断 | 无 Shape、错误选择前置和拓扑调用边界通过测试/构建 | 当前是网格派生拓扑检查,不等价于 OCCT `BRepCheck_Analyzer`,完整修复建议和自交诊断仍待实现 | | PART-07 Shape check | `check-shape` 使用 Facade `geometry.topology()` 生成面/边/顶点数量和 `SHAPE_CHECK_PASSED` 信息诊断;空缓存、空拓扑和 Worker 错误分别返回结构化失败诊断 | 无 Shape、错误选择前置和拓扑调用边界通过测试/构建 | 当前是网格派生拓扑检查,不等价于 OCCT `BRepCheck_Analyzer`,完整修复建议和自交诊断仍待实现 |
| P1-02 命令准确性 | manifest 仍可完整展示Part `primitive/union/cut/intersection` 与 Part Design 核心命令已有 Facade executor命令状态同时检查工作台和对象类型选择谓词其余未接入 executor 的命令返回 disabled 与 `COMMAND_UNIMPLEMENTED`,不再静默完成 | Part 基本体/布尔 Shape 缓存、选择不兼容时的禁用原因、`linear-pattern` 状态与诊断测试通过 | 其余工作台要逐项增加真实 executor、Task 参数和黄金回放后才能启用 | | P1-02 命令准确性 | manifest 仍可完整展示Part `primitive/union/cut/intersection` 与 Part Design 核心命令已有 Facade executor命令状态同时检查工作台和对象类型选择谓词其余未接入 executor 的命令返回 disabled 与 `COMMAND_UNIMPLEMENTED`,不再静默完成 | Part 基本体/布尔 Shape 缓存、选择不兼容时的禁用原因、`linear-pattern` 状态与诊断测试通过 | 其余工作台要逐项增加真实 executor、Task 参数和黄金回放后才能启用 |

View File

@@ -1593,6 +1593,7 @@ P0 基线/治理
| P4-02 Property/Link/Unit | `IN PROGRESS` | DocumentObject 已携带 Data/View 类型化属性元数据;编辑经过 Facade 校验、版本、dirty、Undo/Redo、autosave 和 SQLite `object_properties`;基础表达式/单位/Link DAG 和常用数学函数已落地locale、多选 mixed 和持久引用迁移仍待实现 | | P4-02 Property/Link/Unit | `IN PROGRESS` | DocumentObject 已携带 Data/View 类型化属性元数据;编辑经过 Facade 校验、版本、dirty、Undo/Redo、autosave 和 SQLite `object_properties`;基础表达式/单位/Link DAG 和常用数学函数已落地locale、多选 mixed 和持久引用迁移仍待实现 |
| P2-01/P2-02 SQLite schema 与 Persistence Worker | `IN PROGRESS` | `src/facade/projectSchema.ts` 固定 schema v4`src/facade/persistenceWorker.ts` 独占 SQLite 连接并提供 OPFS/内存模式;`PersistenceWriteQueue` 已保证单写者顺序和失败后续写1000 次压力、迁移回滚和恢复演练仍待补齐;跨标签写入已由 P2-05 通过 Web Locks/降级能力标记覆盖 | | P2-01/P2-02 SQLite schema 与 Persistence Worker | `IN PROGRESS` | `src/facade/projectSchema.ts` 固定 schema v4`src/facade/persistenceWorker.ts` 独占 SQLite 连接并提供 OPFS/内存模式;`PersistenceWriteQueue` 已保证单写者顺序和失败后续写1000 次压力、迁移回滚和恢复演练仍待补齐;跨标签写入已由 P2-05 通过 Web Locks/降级能力标记覆盖 |
| P2-03 OPFS 资源管理 | `IN PROGRESS` | Worker 已提供 SHA-256 内容寻址、`bitbybit-assets` 目录、引用计数、读取和释放;浏览器验证通过,配额治理、垃圾回收扫描和大资源压力仍待补齐 | | P2-03 OPFS 资源管理 | `IN PROGRESS` | Worker 已提供 SHA-256 内容寻址、`bitbybit-assets` 目录、引用计数、读取和释放;浏览器验证通过,配额治理、垃圾回收扫描和大资源压力仍待补齐 |
| Worker memory resource fallback | `PASS` | Worker 在 SQLite WASM 无 OPFS 时使用 transient content-addressed resource map保持 `put/get/release` 引用计数语义;不再把资源 API 错误地绑定到 OPFS |
| P2-04 自动保存调度 | `IN PROGRESS` | `ProjectAutosaveScheduler` 在 Facade 文档事务后按空闲窗口合并最新版本,显式 Save 仍立即写入;`project.recovery(documentId)` 已提供 SQLite/内存完整性、最后保存版本和警告报告,崩溃点检测和用户可配置策略仍待补齐 | | P2-04 自动保存调度 | `IN PROGRESS` | `ProjectAutosaveScheduler` 在 Facade 文档事务后按空闲窗口合并最新版本,显式 Save 仍立即写入;`project.recovery(documentId)` 已提供 SQLite/内存完整性、最后保存版本和警告报告,崩溃点检测和用户可配置策略仍待补齐 |
| Document load transaction | `PASS` | `app.document.load(documentId)` 通过 Project Facade 恢复 SQLite/内存快照,取消旧重算、释放缓存 Shape、清空选择/任务并提交可撤销的活动文档;工程管理页已接入默认本地快照打开流程 | | Document load transaction | `PASS` | `app.document.load(documentId)` 通过 Project Facade 恢复 SQLite/内存快照,取消旧重算、释放缓存 Shape、清空选择/任务并提交可撤销的活动文档;工程管理页已接入默认本地快照打开流程 |
| P2-05 多标签页写入协调 | `DONE (降级可观测)` | `SqliteProjectPersistence` 在保存和资源写入外包 `navigator.locks` 独占锁,成功保存通过 `BroadcastChannel` 广播文档版本Facade 暴露 `project.subscribeExternalChanges()`;无 Web Locks 的运行时能力标记为 `local-queue`,不宣称跨标签页互斥 | | P2-05 多标签页写入协调 | `DONE (降级可观测)` | `SqliteProjectPersistence` 在保存和资源写入外包 `navigator.locks` 独占锁,成功保存通过 `BroadcastChannel` 广播文档版本Facade 暴露 `project.subscribeExternalChanges()`;无 Web Locks 的运行时能力标记为 `local-queue`,不宣称跨标签页互斥 |

View File

@@ -32,6 +32,7 @@ const workerScope = globalThis as unknown as WorkerScope
let sqlite3: Sqlite3Static | undefined let sqlite3: Sqlite3Static | undefined
let database: Database | undefined let database: Database | undefined
let assetsDirectory: FileSystemDirectoryHandle | undefined let assetsDirectory: FileSystemDirectoryHandle | undefined
const transientResources = new Map<string, { resource: ProjectResource; bytes: Uint8Array }>()
let capabilities: PersistenceCapabilities = { mode: 'unavailable', sqliteWasm: false, opfs: false, schemaVersion: PROJECT_SCHEMA_VERSION, reason: 'SQLite WASM has not been initialized.' } 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 cloneTree = (tree: ModelTreeItem[]) => tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
@@ -149,6 +150,12 @@ const recoveryReport = (documentId: string): ProjectRecoveryReport => {
const putResource = async (bytes: ArrayBuffer, mediaType: string): Promise<ProjectResource> => { const putResource = async (bytes: ArrayBuffer, mediaType: string): Promise<ProjectResource> => {
if (!database) throw new Error('Persistence database is not initialized.') if (!database) throw new Error('Persistence database is not initialized.')
const hash = await hashBytes(bytes) const hash = await hashBytes(bytes)
if (!capabilities.opfs) {
const existing = transientResources.get(hash)
const resource = { hash, byteLength: bytes.byteLength, mediaType, refCount: (existing?.resource.refCount || 0) + 1 }
transientResources.set(hash, { resource, bytes: new Uint8Array(bytes.slice(0)) })
return { ...resource }
}
const directory = await getAssetsDirectory() const directory = await getAssetsDirectory()
const handle = await directory.getFileHandle(hash, { create: true }) const handle = await directory.getFileHandle(hash, { create: true })
const writable = await handle.createWritable() const writable = await handle.createWritable()
@@ -162,6 +169,10 @@ const putResource = async (bytes: ArrayBuffer, mediaType: string): Promise<Proje
} }
const getResource = async (hash: string): Promise<ArrayBuffer | null> => { const getResource = async (hash: string): Promise<ArrayBuffer | null> => {
if (!capabilities.opfs) {
const resource = transientResources.get(hash)
return resource ? resource.bytes.slice().buffer : null
}
const directory = await getAssetsDirectory() const directory = await getAssetsDirectory()
try { try {
const handle = await directory.getFileHandle(hash) const handle = await directory.getFileHandle(hash)
@@ -174,6 +185,13 @@ const getResource = async (hash: string): Promise<ArrayBuffer | null> => {
const releaseResource = async (hash: string) => { const releaseResource = async (hash: string) => {
if (!database) throw new Error('Persistence database is not initialized.') if (!database) throw new Error('Persistence database is not initialized.')
if (!capabilities.opfs) {
const existing = transientResources.get(hash)
if (!existing) return
if (existing.resource.refCount > 1) existing.resource.refCount -= 1
else transientResources.delete(hash)
return
}
const resources = database.exec({ sql: 'SELECT ref_count FROM resources WHERE hash = ?', bind: [hash], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number>> const resources = database.exec({ sql: 'SELECT ref_count FROM resources WHERE hash = ?', bind: [hash], rowMode: 'object', returnValue: 'resultRows' }) as Array<Record<string, string | number>>
const row = resources[0] const row = resources[0]
if (!row) return if (!row) return
@@ -188,7 +206,7 @@ const releaseResource = async (hash: string) => {
const handle = async (request: PersistenceRequest): Promise<PersistenceResponse> => { const handle = async (request: PersistenceRequest): Promise<PersistenceResponse> => {
try { try {
if (request.type === 'initialize') return { id: request.id, ok: true, type: 'initialized', capabilities: await initialize() } 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' } } if (request.type === 'dispose') { database?.close(); database = undefined; transientResources.clear(); return { id: request.id, ok: true, type: 'disposed' } }
await initialize() await initialize()
if (request.type === 'save-document') { const result = saveDocument(request.document); return { id: request.id, ok: true, type: 'saved', ...result } } if (request.type === 'save-document') { const result = saveDocument(request.document); return { id: request.id, ok: true, type: 'saved', ...result } }
if (request.type === 'load-document') return { id: request.id, ok: true, type: 'loaded', document: loadDocument(request.documentId) } if (request.type === 'load-document') return { id: request.id, ok: true, type: 'loaded', document: loadDocument(request.documentId) }