fix: support worker memory resource fallback
This commit is contained in:
@@ -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、属性数量和类型;未知类型为 proxy,Python 类型为 blocked;脚本资源只告警且不执行 | recognized/proxy/blocked 分类和 Facade-only 测试通过 | `metadata-compatible` 仅表示 XML 元数据可读,不表示 FCStd 可编辑或 round-trip |
|
| FC-04/FC-07 只读报告 | `project.fcstd.inspect()` 解析 Document.xml 对象声明、Label、属性数量和类型;未知类型为 proxy,Python 类型为 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 参数和黄金回放后才能启用 |
|
||||||
|
|
||||||
|
|||||||
@@ -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`,不宣称跨标签页互斥 |
|
||||||
|
|||||||
@@ -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) }
|
||||||
|
|||||||
Reference in New Issue
Block a user