feat: define versioned sketch solver protocol

This commit is contained in:
2026-08-02 23:49:35 -04:00
parent 4d94109627
commit e0b4817ec0
6 changed files with 227 additions and 3 deletions

View File

@@ -19,7 +19,7 @@
"facadeCapabilities": { "facadeCapabilities": {
"geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] }, "geometry": { "level": "experimental", "provider": "BitBybit OCCT 1.1.1", "operations": ["box", "cylinder", "sphere", "cone", "placement", "union", "cut", "intersection", "part-primitive-recompute", "part-boolean-recompute", "shape-check", "pad", "pocket", "pocket-through-all", "revolution", "fillet", "chamfer", "linear-pattern-whole-shape", "polar-pattern-whole-shape", "hole-basic-origin-normal", "feature-shape-cache", "face-edge-vertex-topology", "step-export", "stl-export"] },
"document": { "level": "experimental", "operations": ["typed-properties", "property-link-sub", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "versioned-toporef", "toporef-migration", "signature-topology-history-fallback", "undo-redo"] }, "document": { "level": "experimental", "operations": ["typed-properties", "property-link-sub", "expressions", "units", "dependency-dag", "parallel-level-recompute", "sync-recompute", "async-generation-recompute", "root-cause-diagnostics", "repair-branch-recompute", "versioned-toporef", "toporef-migration", "signature-topology-history-fallback", "undo-redo"] },
"sketcher": { "level": "experimental", "operations": ["point-line-circle-arc-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "persistence"] }, "sketcher": { "level": "experimental", "provider": "typescript-basic; planegcs-wasm unavailable", "operations": ["point-line-circle-arc-model", "basic-constraints", "diameter-symmetric-tangent", "basic-solver", "versioned-solver-protocol", "solver-capability-probe", "cancel-stale-isolation", "replay-contract", "persistence"] },
"fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "proxy-report", "script-isolation"] } "fcstd": { "level": "read-only", "operations": ["zip-preflight", "document-xml-metadata", "proxy-report", "script-isolation"] }
}, },
"rules": [ "rules": [

View File

@@ -328,7 +328,7 @@ Geometry Facade 还接入了 Bitbybit IO 的 STEP 和 ASCII STL 导出,作为
| DAG-08 失败/抑制传播 | 上游 `error` 将下游置为 `upstream-failed`Feature 的可持久化 `Suppressed` 属性将自身置为 `suppressed`、下游置为 `upstream-suppressed`,不误报重算失败;抑制时释放缓存 Shape解除后只重算依赖闭包重算错误映射为带版本、generation、根因、依赖路径和修复动作的诊断树 | 上游失败、抑制跳过、Shape 释放、保存恢复、根因树、对象定位和解除错误后的最小闭包修复测试通过 | 多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和完整 FreeCAD 状态机仍待实现 | | DAG-08 失败/抑制传播 | 上游 `error` 将下游置为 `upstream-failed`Feature 的可持久化 `Suppressed` 属性将自身置为 `suppressed`、下游置为 `upstream-suppressed`,不误报重算失败;抑制时释放缓存 Shape解除后只重算依赖闭包重算错误映射为带版本、generation、根因、依赖路径和修复动作的诊断树 | 上游失败、抑制跳过、Shape 释放、保存恢复、根因树、对象定位和解除错误后的最小闭包修复测试通过 | 多配置抑制、Body Tip 重定向、最近有效 Shape 显式回滚和完整 FreeCAD 状态机仍待实现 |
| P4-06 任务提交与重算闭包 | Task 草稿在 Facade 事务内写入 Boolean Base/Tool 等链接;新对象和依赖闭包进入 `touched/dirty`React OK 通过 `recomputeAsync()` 提交并反馈结果Facade `diagnostics` 提供根因树和定位/闭包重算/合法抑制修复Diagnostics 页面只做事件投影 | Boolean Task 链接/依赖测试、Task dirty 快照、根因路径和修复后诊断清理通过 | Apply 预览仍未持久化临时 Shape多选选择集、Shape 回滚和完整 FreeCAD Task 校验仍待实现 | | P4-06 任务提交与重算闭包 | Task 草稿在 Facade 事务内写入 Boolean Base/Tool 等链接;新对象和依赖闭包进入 `touched/dirty`React OK 通过 `recomputeAsync()` 提交并反馈结果Facade `diagnostics` 提供根因树和定位/闭包重算/合法抑制修复Diagnostics 页面只做事件投影 | Boolean Task 链接/依赖测试、Task dirty 快照、根因路径和修复后诊断清理通过 | Apply 预览仍未持久化临时 Shape多选选择集、Shape 回滚和完整 FreeCAD Task 校验仍待实现 |
| DAG-07 Body/Tip 与来源链接 | Part Design 特征提交时按来源类型绑定 Profile/Base更新只读 Body.Tip并将 Body 与新特征一并纳入 dirty/recompute 闭包Undo/Redo 恢复 Tip | Pad Task、Body.Tip、Undo/Redo 回归测试通过 | 单实体规则、Tip 抑制/重定向、复杂容器和跨 Body 支持仍待实现 | | DAG-07 Body/Tip 与来源链接 | Part Design 特征提交时按来源类型绑定 Profile/Base更新只读 Body.Tip并将 Body 与新特征一并纳入 dirty/recompute 闭包Undo/Redo 恢复 Tip | Pad Task、Body.Tip、Undo/Redo 回归测试通过 | 单实体规则、Tip 抑制/重定向、复杂容器和跨 Body 支持仍待实现 |
| SK-04 求解器边界 | Sketch 节点通过可替换 executor 求解;冲突/无效约束阻止下游,求解结果作为对象更新返回 | 基础 Sketch solve 与异步对象更新测试通过 | 当前是基础 TypeScript solver,不是 `planegcs` WASM;高级约束、拖拽、外部几何和 B-spline 未完成 | | SK-04 求解器边界 | `DONE`:版本化请求/响应、provider 能力探测、取消/过期 generation 隔离和回放合同已落地 | 基础 provider 回放、并发取消和文档版本过期测试通过 | 当前是基础 TypeScript solver`planegcs-wasm` provider 明确报告 unavailable不虚报 SK-03 完成;高级约束、拖拽、外部几何和 B-spline 未完成 |
| 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 Project list/load | `project.list()` 暴露 SQLite/内存快照摘要,`app.document.load()` 将选定快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态、保留 Undo 边界并按完整对象图异步重算Projects 页面按 documentId 调用 Facade API | 列表摘要、保存→修改→加载后属性恢复、选择清空、任务清空和加载重算调用通过测试 | FCStd 导入仍是只读 metadata/proxy跨文档 ShapeHandle 和 Shape 持久化仍待完成 | | P2-04 Project list/load | `project.list()` 暴露 SQLite/内存快照摘要,`app.document.load()` 将选定快照作为活动 Document 事务恢复,清理旧重算/Shape/Task 状态、保留 Undo 边界并按完整对象图异步重算Projects 页面按 documentId 调用 Facade API | 列表摘要、保存→修改→加载后属性恢复、选择清空、任务清空和加载重算调用通过测试 | FCStd 导入仍是只读 metadata/proxy跨文档 ShapeHandle 和 Shape 持久化仍待完成 |

View File

@@ -1725,7 +1725,7 @@ DocumentSnapshot 现在区分模型树投影与 `DocumentObjectSnapshot` 真值
尚未完成的 TSN 工作OCCT `Generated/Modified/Deleted` 历史捕获、跨布尔/特征的真实拓扑映射、面/边/顶点统一命名、附着和 `TopoRef` 文件迁移。当前签名是可审计的基础候选层,不能单独宣称 FreeCAD 稳定拓扑命名已完成。 尚未完成的 TSN 工作OCCT `Generated/Modified/Deleted` 历史捕获、跨布尔/特征的真实拓扑映射、面/边/顶点统一命名、附着和 `TopoRef` 文件迁移。当前签名是可审计的基础候选层,不能单独宣称 FreeCAD 稳定拓扑命名已完成。
Sketcher 当前边界:基础适配器支持点/线/圆/弧、Coincident/Horizontal/Vertical/Distance/Radius/Diameter/Angle/Equal/Symmetric/Tangent/Block 的确定性约束处理;完整 FreeCAD `planegcs` 数值求解、冗余约束分类、B-spline、外部几何、自动约束和拖拽交互仍需 SK-03、SK-07、SK-09、SK-10 的 WASM/浏览器回放门禁。 Sketcher 当前边界:基础适配器支持点/线/圆/弧、Coincident/Horizontal/Vertical/Distance/Radius/Diameter/Angle/Equal/Symmetric/Tangent/Block 的确定性约束处理;SK-04 已增加版本化 provider 协议、能力探测、取消/过期结果隔离和回放合同。仓库没有 FreeCAD 源码或 planegcs WASM 产物,因此对应 provider 明确返回 unavailable完整 FreeCAD `planegcs` 数值求解、冗余约束分类、B-spline、外部几何、自动约束和拖拽交互仍需 SK-03、SK-07、SK-09、SK-10 的 WASM/浏览器回放门禁。
### 16.16 引擎版本修复与完整 FreeCAD 对标专项 ### 16.16 引擎版本修复与完整 FreeCAD 对标专项

View File

@@ -12,6 +12,8 @@ export { captureSignatureTopologyHistory } from './topologyHistory'
export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory' export type { TopologyHistoryEntry, TopologyHistoryRelation, TopologyHistoryResult } from './topologyHistory'
export { BasicSketchSolverAdapter, cloneSketch, createSketch, solveSketch } from './sketcher' export { BasicSketchSolverAdapter, cloneSketch, createSketch, solveSketch } from './sketcher'
export type { SketchConstraint, SketchDiagnostic, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher' export type { SketchConstraint, SketchDiagnostic, SketchGeometry, SketchPoint, SketchPointRef, SketchSnapshot, SketchSolveOptions, SketchSolveResult, SketchSolverAdapter, SketchSolverStatus } from './sketcher'
export { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay } from './sketchSolverProtocol'
export type { SketchSolverCapabilities, SketchSolverCompatibility, SketchSolverExecution, SketchSolverProvider, SketchSolverReplayCase, SketchSolverReplayResult, SketchSolverRequest, SketchSolverResponse } from './sketchSolverProtocol'
export { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator } from './recomputeEngine' export { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator } from './recomputeEngine'
export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine' export type { RecomputeExecutionError, RecomputeExecutionOptions, RecomputeExecutionResult, RecomputeExecutionStatus, RecomputeGeometryRuntime, RecomputeNodeContext, RecomputeNodeExecutor, RecomputeNodeResult, RecomputeProgress } from './recomputeEngine'
export { DEFAULT_FCSTD_LIMITS, inspectFcstdArchive } from './fcstd' export { DEFAULT_FCSTD_LIMITS, inspectFcstdArchive } from './fcstd'

View File

@@ -0,0 +1,174 @@
import { cloneSketch, solveSketch, type SketchSnapshot, type SketchSolveOptions, type SketchSolveResult } from './sketcher'
export const SKETCH_SOLVER_PROTOCOL_VERSION = 1 as const
export type SketchSolverCompatibility = 'experimental' | 'freecad-1.1.1'
export type SketchSolverCapabilities = {
providerId: string
providerVersion: string
engine: 'typescript-basic' | 'planegcs-wasm'
availability: 'available' | 'unavailable'
compatibility: SketchSolverCompatibility
supportedGeometry: string[]
supportedConstraints: string[]
reason?: string
}
export type SketchSolverRequest = {
protocolVersion: typeof SKETCH_SOLVER_PROTOCOL_VERSION
requestId: string
documentId: string
documentVersion: number
generation: number
snapshot: SketchSnapshot
options?: SketchSolveOptions
}
export type SketchSolverResponse = {
protocolVersion: typeof SKETCH_SOLVER_PROTOCOL_VERSION
requestId: string
documentId: string
documentVersion: number
generation: number
provider: SketchSolverCapabilities
result: SketchSolveResult
}
export interface SketchSolverProvider {
capabilities(): SketchSolverCapabilities
solve(request: SketchSolverRequest, signal: AbortSignal): Promise<SketchSolverResponse>
}
export class SketchSolverUnavailableError extends Error {
readonly code = 'SKETCH_SOLVER_UNAVAILABLE'
constructor(readonly capabilities: SketchSolverCapabilities) {
super(capabilities.reason || `Sketch solver provider '${capabilities.providerId}' is unavailable.`)
this.name = 'SketchSolverUnavailableError'
}
}
const assertRequest = (request: SketchSolverRequest) => {
if (request.protocolVersion !== SKETCH_SOLVER_PROTOCOL_VERSION) throw new RangeError(`Unsupported sketch solver protocol version: ${request.protocolVersion}.`)
if (!request.requestId || !request.documentId) throw new TypeError('Sketch solver request requires requestId and documentId.')
if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Sketch solver documentVersion must be a non-negative integer.')
if (!Number.isSafeInteger(request.generation) || request.generation < 1) throw new RangeError('Sketch solver generation must be a positive integer.')
}
const abortError = () => new DOMException('Sketch solve cancelled.', 'AbortError')
export class BasicSketchSolverProvider implements SketchSolverProvider {
capabilities(): SketchSolverCapabilities {
return {
providerId: 'web-cad.basic-sketch-solver',
providerVersion: '1.0.0',
engine: 'typescript-basic',
availability: 'available',
compatibility: 'experimental',
supportedGeometry: ['point', 'line', 'circle', 'arc'],
supportedConstraints: ['coincident', 'horizontal', 'vertical', 'distance', 'distanceX', 'distanceY', 'radius', 'diameter', 'angle', 'equal', 'symmetric', 'tangent', 'block'],
}
}
async solve(request: SketchSolverRequest, signal: AbortSignal): Promise<SketchSolverResponse> {
assertRequest(request)
if (signal.aborted) throw abortError()
const result = solveSketch(cloneSketch(request.snapshot), request.options)
if (signal.aborted) throw abortError()
return {
protocolVersion: request.protocolVersion,
requestId: request.requestId,
documentId: request.documentId,
documentVersion: request.documentVersion,
generation: request.generation,
provider: this.capabilities(),
result,
}
}
}
export class UnavailablePlanegcsProvider implements SketchSolverProvider {
constructor(private readonly reason = 'No FreeCAD 1.1.1 planegcs WASM artifact is installed.') {}
capabilities(): SketchSolverCapabilities {
return {
providerId: 'freecad.planegcs-wasm',
providerVersion: 'unavailable',
engine: 'planegcs-wasm',
availability: 'unavailable',
compatibility: 'experimental',
supportedGeometry: [],
supportedConstraints: [],
reason: this.reason,
}
}
solve(): Promise<SketchSolverResponse> {
return Promise.reject(new SketchSolverUnavailableError(this.capabilities()))
}
}
export type SketchSolverExecution = {
status: 'completed' | 'cancelled' | 'stale'
generation: number
response?: SketchSolverResponse
}
export class SketchSolverCoordinator {
private active: { generation: number; controller: AbortController } | null = null
private generation = 0
constructor(private readonly currentDocumentVersion: (documentId: string) => number | null) {}
cancel() { this.active?.controller.abort() }
async solve(provider: SketchSolverProvider, input: Omit<SketchSolverRequest, 'protocolVersion' | 'generation'>): Promise<SketchSolverExecution> {
this.active?.controller.abort()
const generation = ++this.generation
const controller = new AbortController()
this.active = { generation, controller }
const request: SketchSolverRequest = { ...input, protocolVersion: SKETCH_SOLVER_PROTOCOL_VERSION, generation, snapshot: cloneSketch(input.snapshot) }
try {
const response = await provider.solve(request, controller.signal)
if (controller.signal.aborted || this.active?.generation !== generation) return { status: 'cancelled', generation }
if (this.currentDocumentVersion(input.documentId) !== input.documentVersion) return { status: 'stale', generation }
return { status: 'completed', generation, response }
} catch (error) {
if (controller.signal.aborted || (error instanceof Error && error.name === 'AbortError')) return { status: 'cancelled', generation }
throw error
} finally {
if (this.active?.generation === generation) this.active = null
}
}
}
export type SketchSolverReplayCase = {
id: string
request: Omit<SketchSolverRequest, 'protocolVersion' | 'generation'>
expected: {
status: SketchSolveResult['status']
degreesOfFreedom: number
maxResidual: number
}
}
export type SketchSolverReplayResult = { id: string; passed: boolean; differences: string[] }
export const runSketchSolverReplay = async (provider: SketchSolverProvider, cases: SketchSolverReplayCase[]): Promise<SketchSolverReplayResult[]> => {
const coordinator = new SketchSolverCoordinator(() => 1)
const results: SketchSolverReplayResult[] = []
for (const fixture of cases) {
const execution = await coordinator.solve(provider, fixture.request)
const actual = execution.response?.result
const differences: string[] = []
if (!actual) differences.push(`execution status was ${execution.status}`)
else {
if (actual.status !== fixture.expected.status) differences.push(`status: expected ${fixture.expected.status}, received ${actual.status}`)
if (actual.degreesOfFreedom !== fixture.expected.degreesOfFreedom) differences.push(`degreesOfFreedom: expected ${fixture.expected.degreesOfFreedom}, received ${actual.degreesOfFreedom}`)
if (actual.residual > fixture.expected.maxResidual) differences.push(`residual: expected <= ${fixture.expected.maxResidual}, received ${actual.residual}`)
}
results.push({ id: fixture.id, passed: differences.length === 0, differences })
}
return results
}

View File

@@ -10,6 +10,7 @@ import { createEdgeSubshapeRefs, createSubshapeRefs, createVertexSubshapeRefs, m
import { createPersistedTopoRef, migrateTopoRefs, parseTopoRef, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences' import { createPersistedTopoRef, migrateTopoRefs, parseTopoRef, resolveTopoRef, serializeTopoRef } from '../src/facade/topologyReferences'
import { captureSignatureTopologyHistory } from '../src/facade/topologyHistory' import { captureSignatureTopologyHistory } from '../src/facade/topologyHistory'
import { createSketch, solveSketch } from '../src/facade/sketcher' import { createSketch, solveSketch } from '../src/facade/sketcher'
import { BasicSketchSolverProvider, SKETCH_SOLVER_PROTOCOL_VERSION, SketchSolverCoordinator, SketchSolverUnavailableError, UnavailablePlanegcsProvider, runSketchSolverReplay, type SketchSolverProvider, type SketchSolverRequest } from '../src/facade/sketchSolverProtocol'
import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine' import { createFacadeGeometryRecomputeExecutor, executeFacadeRecomputeNode, RecomputeCoordinator, type RecomputeGeometryRuntime } from '../src/facade/recomputeEngine'
import { inspectFcstdArchive } from '../src/facade/fcstd' import { inspectFcstdArchive } from '../src/facade/fcstd'
import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime' import { assertShapeHandleIntegrity, normalizeBitbybitMesh, validateBooleanUnionInput, validateBoxInput, validateChamferInput, validateConeInput, validateCylinderInput, validateFilletInput, validatePadInput, validatePlacementInput, validatePlanarProfile, validateRevolutionInput, validateSphereInput } from '../src/facade/geometryRuntime'
@@ -254,6 +255,53 @@ test('Sketcher solver supports diameter, symmetric and tangent constraints at th
assert.equal((result.snapshot.geometry.find((geometry) => geometry.id === 'circle-1') as Extract<typeof result.snapshot.geometry[number], { type: 'circle' }>).center.y, 2) assert.equal((result.snapshot.geometry.find((geometry) => geometry.id === 'circle-1') as Extract<typeof result.snapshot.geometry[number], { type: 'circle' }>).center.y, 2)
}) })
test('sketch solver protocol exposes honest capabilities and deterministic replay', async () => {
const provider = new BasicSketchSolverProvider()
assert.deepEqual(provider.capabilities().engine, 'typescript-basic')
assert.equal(provider.capabilities().compatibility, 'experimental')
const fixture = createSketch('protocol-sketch', [{ id: 'line-1', type: 'line', start: { x: 0, y: 0 }, end: { x: 4, y: 2 } }], [
{ id: 'horizontal-1', type: 'horizontal', geometryId: 'line-1' },
])
const replay = await runSketchSolverReplay(provider, [{
id: 'basic-horizontal',
request: { requestId: 'replay-1', documentId: 'doc', documentVersion: 1, snapshot: fixture },
expected: { status: 'under-constrained', degreesOfFreedom: 3, maxResidual: 1e-7 },
}])
assert.deepEqual(replay, [{ id: 'basic-horizontal', passed: true, differences: [] }])
const unavailable = new UnavailablePlanegcsProvider()
assert.equal(unavailable.capabilities().availability, 'unavailable')
assert.equal(unavailable.capabilities().engine, 'planegcs-wasm')
await assert.rejects(() => unavailable.solve({} as SketchSolverRequest, new AbortController().signal), SketchSolverUnavailableError)
})
test('sketch solver coordinator rejects cancelled and stale generations', async () => {
let documentVersion = 1
let releaseFirst: (() => void) | undefined
const basic = new BasicSketchSolverProvider()
const delayed: SketchSolverProvider = {
capabilities: () => basic.capabilities(),
solve: (request, signal) => new Promise((resolve, reject) => {
const release = () => basic.solve(request, signal).then(resolve, reject)
if (request.requestId === 'first') releaseFirst = release
else release()
}),
}
const coordinator = new SketchSolverCoordinator(() => documentVersion)
const snapshot = createSketch('coordinator')
const first = coordinator.solve(delayed, { requestId: 'first', documentId: 'doc', documentVersion: 1, snapshot })
const second = coordinator.solve(delayed, { requestId: 'second', documentId: 'doc', documentVersion: 1, snapshot })
releaseFirst?.()
assert.equal((await first).status, 'cancelled')
assert.equal((await second).status, 'completed')
documentVersion = 2
const stale = await coordinator.solve(basic, { requestId: 'stale', documentId: 'doc', documentVersion: 1, snapshot })
assert.equal(stale.status, 'stale')
assert.equal(stale.generation, 3)
assert.equal(SKETCH_SOLVER_PROTOCOL_VERSION, 1)
})
test('sketcher operations are facade transactions and survive the project fallback store', async () => { test('sketcher operations are facade transactions and survive the project fallback store', async () => {
const facade = createMockFacade() const facade = createMockFacade()
const before = facade.app.document.getActive().version const before = facade.app.document.getActive().version