Compare commits
3 Commits
a3bbd5638a
...
5cc4f96b46
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cc4f96b46 | |||
| 7b1c87f2a4 | |||
| fea1fe84bf |
35
.trae/documents/decision-remove-preset.md
Normal file
35
.trae/documents/decision-remove-preset.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# 决策:移除 ArrayTracer 的 preset 指令
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
在设计 ArrayTracer 的指令集时,我们讨论了如何初始化数组数据。最初设计了 `preset` 指令用于预设数组内容,后来讨论了是否将其与 `create` 指令合并。
|
||||||
|
|
||||||
|
## 问题分析
|
||||||
|
|
||||||
|
保留独立的 `preset` 指令,特别是允许其出现在 `patch` 等操作指令之后,会带来以下系统性风险:
|
||||||
|
|
||||||
|
### 1. 语义与逻辑的二义性
|
||||||
|
|
||||||
|
* **预设 vs 重置**:`preset` 本意为“预先设置”,若出现在操作流中间,语义突变为“重置”或“全量替换”,导致语义混乱。
|
||||||
|
|
||||||
|
* **状态突变**:解析器需同时处理增量更新(Patch)和全量覆盖(Preset)。若 `preset` 改变数组长度,会导致后续基于索引的回溯操作(Time Travel)失效或越界。
|
||||||
|
|
||||||
|
### 2. 可视化渲染的连续性问题
|
||||||
|
|
||||||
|
* **动画中断**:`patch` 对应平滑过渡,而 `preset` 对应全量重绘。在中间插入 `preset` 会导致渲染器难以生成补间动画,造成视觉跳变。
|
||||||
|
|
||||||
|
* **对象身份丢失**:全量替换会导致 DOM 元素及其状态(如高亮)丢失,用户体验突兀。
|
||||||
|
|
||||||
|
### 3. “时间旅行”的性能代价
|
||||||
|
|
||||||
|
* **回滚成本高**:回滚 `patch` 仅需反向操作,而回滚 `preset` 需要在执行前对整个大数组进行全量快照(Snapshot)。若 `preset` 频繁出现,内存消耗将不可控。
|
||||||
|
|
||||||
|
## 决策结论
|
||||||
|
|
||||||
|
**移除独立的** **`preset`** **指令,将其能力作为可选参数** **`array`** **合并入** **`create`** **指令。**
|
||||||
|
|
||||||
|
### 收益
|
||||||
|
|
||||||
|
1. **生命周期明确**:强制确立“初始化(Create) -> 演变(Evolve)”的生命周期,消除“中途重置”的不确定性。
|
||||||
|
2. **系统简化**:解析器仅需处理增量更新,渲染器无需处理全量 DOM 替换,历史记录无需重型快照。
|
||||||
|
|
||||||
@@ -10,12 +10,15 @@
|
|||||||
|
|
||||||
1. **优先使用构造函数参数**:如果语言支持(如 Python, Kotlin, Swift),优先使用带默认值的命名参数。
|
1. **优先使用构造函数参数**:如果语言支持(如 Python, Kotlin, Swift),优先使用带默认值的命名参数。
|
||||||
2. **利用语言特性**:
|
2. **利用语言特性**:
|
||||||
- **重载 (Overloading)**:适用于 Java, C++, C#。
|
|
||||||
- **配置对象 (Options Object)**:适用于 TS, JS, Lua。
|
* **重载 (Overloading)**:适用于 Java, C++, C#。
|
||||||
- **Builder / Functional Options**:适用于 Go, Rust。
|
|
||||||
|
* **配置对象 (Options Object)**:适用于 TS, JS, Lua。
|
||||||
|
|
||||||
|
* **Builder / Functional Options**:适用于 Go, Rust。
|
||||||
3. **保持协议底层一致**:无论上层 API 如何设计,底层生成的 `create` 指令 JSON 必须包含 `array` (或对应数据字段) 参数。
|
3. **保持协议底层一致**:无论上层 API 如何设计,底层生成的 `create` 指令 JSON 必须包含 `array` (或对应数据字段) 参数。
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 各语言实现参考
|
## 各语言实现参考
|
||||||
|
|
||||||
@@ -217,8 +220,8 @@ Point pts[] = {{1,2}, {3,4}};
|
|||||||
tracer_t* t = tracer_create_array_custom("Points", pts, 2, sizeof(Point), point_serializer);
|
tracer_t* t = tracer_create_array_custom("Points", pts, 2, sizeof(Point), point_serializer);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
***
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
通过统一采用**“构造即初始化”**的设计模式,我们能够在几乎所有主流编程语言中提供一致、简洁且符合语言习惯(Idiomatic)的 SDK 使用体验。这不仅降低了用户的学习成本,也使得代码更加紧凑和易读。
|
通过统一采用\*\*“构造即初始化”\*\*的设计模式,我们能够在几乎所有主流编程语言中提供一致、简洁且符合语言习惯(Idiomatic)的 SDK 使用体验。这不仅降低了用户的学习成本,也使得代码更加紧凑和易读。
|
||||||
|
|||||||
54
.trae/documents/tracer-validation.md
Normal file
54
.trae/documents/tracer-validation.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Tracer 指令校验机制设计决策
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
在设计 Tracer 协议时,我们面临一个关键的架构决策:**应该在哪里对 Tracer 的操作指令进行合法性校验(例如数组越界、空指针访问)?**
|
||||||
|
|
||||||
|
我们有两个主要的选择:
|
||||||
|
1. **SDK 侧(生产者)**:在用户调用 SDK API(如 `tracer.pick(i)`)生成指令时即时校验。
|
||||||
|
2. **渲染器侧(消费者)**:SDK 只负责生成指令,由前端渲染器在回放指令序列时进行校验。
|
||||||
|
|
||||||
|
## 决策:SDK 侧“影子状态”校验
|
||||||
|
|
||||||
|
经过讨论,我们决定采用 **SDK 侧校验** 作为主要防线,并引入 **“影子状态(Shadow State)”** 模式来实现。
|
||||||
|
|
||||||
|
### 核心设计原则
|
||||||
|
|
||||||
|
1. **Fail Fast(快速失败)**:
|
||||||
|
* 用户的逻辑错误(如访问越界)应在代码执行阶段立即抛出异常,而不是等到生成了错误的 JSON 并在渲染器播放时才报错。
|
||||||
|
* 这样可以将错误精确定位到用户代码的具体行号,极大提升调试体验。
|
||||||
|
|
||||||
|
2. **最小必要状态(Minimal Viable State)**:
|
||||||
|
* SDK 不需要维护完整的数据结构副本(那会带来巨大的内存和性能开销)。
|
||||||
|
* SDK 只需要维护用于校验合法性的 **元数据(Metadata)**。
|
||||||
|
* **案例**:对于 `ArrayTracer`,我们不需要存储数组的具体元素值,只需要维护一个整数 `currentSize`。
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
|
||||||
|
#### 影子状态维护
|
||||||
|
在每个 Tracer 实例内部维护一个轻量级的状态变量:
|
||||||
|
|
||||||
|
* **ArrayTracer**: 维护 `int currentSize`。
|
||||||
|
* `create(array)`: 初始化 `currentSize = array.length`。
|
||||||
|
* `scale(newSize)`: 更新 `currentSize = newSize`。
|
||||||
|
* `pick/patch(index)`: 校验 `0 <= index < currentSize`。
|
||||||
|
|
||||||
|
#### 跨语言适应性
|
||||||
|
这种模式具有极强的跨语言通用性:
|
||||||
|
* **TypeScript/JS**: 简单变量。
|
||||||
|
* **Java/C#**: 类的私有成员字段。
|
||||||
|
* **C/C++**: 结构体中的字段(即便 C 语言原生数组不带长度,通过 Tracer 封装后反而赋予了其边界检查能力)。
|
||||||
|
|
||||||
|
### 对比分析
|
||||||
|
|
||||||
|
| 维度 | SDK 侧校验(采用方案) | 渲染器侧校验 |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **错误反馈时机** | **即时**(用户运行代码时) | **延迟**(用户观看回放时) |
|
||||||
|
| **错误定位能力** | **高**(直接抛出异常,有堆栈信息) | **低**(难以关联回源码行号) |
|
||||||
|
| **实现复杂度** | 中(需维护影子状态) | 低(仅需防御性编程) |
|
||||||
|
| **性能开销** | **极低**(仅维护元数据,如 `int`) | 无额外开销 |
|
||||||
|
| **用户体验** | 类似本地调试,符合直觉 | 可能会看到“崩坏”的动画 |
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
虽然渲染器依然需要具备防御性编程能力以防止崩溃,但**业务逻辑的正确性校验应当由 SDK 在指令生成源头保证**。这不仅符合“数据源头治理”的原则,更能为用户提供更友好的开发和调试环境。
|
||||||
@@ -3,6 +3,14 @@ import type { TracerCommand } from '../types';
|
|||||||
const createTracerContext = () => {
|
const createTracerContext = () => {
|
||||||
const commands: TracerCommand[] = [];
|
const commands: TracerCommand[] = [];
|
||||||
|
|
||||||
|
if (typeof process !== 'undefined' && typeof process.on === 'function') {
|
||||||
|
process.on('exit', () => {
|
||||||
|
if (commands.length > 0) {
|
||||||
|
console.log(commands);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const getTracerContext = () => {
|
const getTracerContext = () => {
|
||||||
const command = (command: TracerCommand) => {
|
const command = (command: TracerCommand) => {
|
||||||
commands.push(command);
|
commands.push(command);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getTracerContext } from './context';
|
|
||||||
import {
|
import {
|
||||||
createArrayTracer,
|
createArrayTracer,
|
||||||
createControlTracer,
|
createControlTracer,
|
||||||
@@ -15,6 +14,6 @@ const arrayTracer = createArrayTracer({
|
|||||||
|
|
||||||
arrayTracer.patch(0, 100);
|
arrayTracer.patch(0, 100);
|
||||||
|
|
||||||
controlTracer.step();
|
logTracer.log(`patch array[0] to 100`);
|
||||||
|
|
||||||
console.log(getTracerContext().commands);
|
controlTracer.step();
|
||||||
|
|||||||
@@ -12,8 +12,20 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
const { description = 'ArrayTracer', array } = options;
|
const { description = 'ArrayTracer', array } = options;
|
||||||
const tracer = crypto.randomUUID();
|
const tracer = crypto.randomUUID();
|
||||||
|
|
||||||
|
// 优化:仅维护数组长度作为影子状态,这在 C++/Java 等强类型语言中也极易实现(仅需一个 int 变量)
|
||||||
|
// 这种“最小必要状态”策略既能实现越界校验,又避免了在强类型语言中处理泛型存储的复杂性,且内存开销极低。
|
||||||
|
let arrayLength = array ? array.length : 0;
|
||||||
|
|
||||||
const { command } = getTracerContext();
|
const { command } = getTracerContext();
|
||||||
|
|
||||||
|
const validateIndex = (index: number) => {
|
||||||
|
if (index < 0 || index >= arrayLength) {
|
||||||
|
throw new Error(
|
||||||
|
`[ArrayTracer] Index out of bounds: index ${index} is not within [0, ${arrayLength})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
@@ -24,7 +36,15 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// size 为正时,数组长度增加;为负时,数组长度减少(但不能小于 0)
|
||||||
const scale = (size: number) => {
|
const scale = (size: number) => {
|
||||||
|
arrayLength += size;
|
||||||
|
if (arrayLength < 0) {
|
||||||
|
throw new Error(
|
||||||
|
`[ArrayTracer] Invalid size: ${size}, array length cannot be negative`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
@@ -36,6 +56,8 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pick = (index: number) => {
|
const pick = (index: number) => {
|
||||||
|
validateIndex(index);
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
@@ -47,6 +69,8 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const drop = (index: number) => {
|
const drop = (index: number) => {
|
||||||
|
validateIndex(index);
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
@@ -58,6 +82,8 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const patch = (index: number, value: T[number]) => {
|
const patch = (index: number, value: T[number]) => {
|
||||||
|
validateIndex(index);
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
@@ -70,6 +96,8 @@ export const createArrayTracer = <T extends JsonValue[]>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const unset = (index: number) => {
|
const unset = (index: number) => {
|
||||||
|
validateIndex(index);
|
||||||
|
|
||||||
command({
|
command({
|
||||||
type: 'ArrayTracer',
|
type: 'ArrayTracer',
|
||||||
tracer: tracer,
|
tracer: tracer,
|
||||||
|
|||||||
Reference in New Issue
Block a user