@transactional-reducer/core 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +590 -0
- package/package.json +12 -10
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anuoua
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
# @transactional-reducer/core
|
|
2
|
+
|
|
3
|
+
为 reducer 模式提供事务(Transaction)支持的状态管理引擎。允许你将一组 dispatch 操作包裹在事务中,支持**提交(commit)**和**回滚(rollback)**,就像数据库事务一样。
|
|
4
|
+
|
|
5
|
+
框架无关——可用于 React、Vue、Node.js 或任何 JavaScript 环境。
|
|
6
|
+
|
|
7
|
+
## 核心价值
|
|
8
|
+
|
|
9
|
+
- **乐观更新 + 自动回滚**:先乐观地更新状态,异步操作失败时自动撤销变更
|
|
10
|
+
- **可取消的异步任务**:相同 id 的事务自动取消前一个,避免竞态条件;`onCancel` 支持在取消时主动清理资源(如中止网络请求)
|
|
11
|
+
- **灵活的去重策略**:`onDuplicate` 支持四种策略——`rollback`(回滚旧事务)、`commit`(提交旧事务)、`reuse`(复用旧事务)、`reject`(拒绝创建)
|
|
12
|
+
- **嵌套事务**:支持父子事务,子事务可独立提交或随父事务回滚
|
|
13
|
+
- **提交边界**:`onError: "commit"` 的子事务在父事务回滚时被保留,实现"部分成功"语义
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @transactional-reducer/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { TransactionalReducer } from "@transactional-reducer/core";
|
|
25
|
+
|
|
26
|
+
type State = { count: number };
|
|
27
|
+
type Action = { type: "inc" } | { type: "dec" };
|
|
28
|
+
|
|
29
|
+
const reducer = (state: State, action: Action): State => {
|
|
30
|
+
switch (action.type) {
|
|
31
|
+
case "inc": return { count: state.count + 1 };
|
|
32
|
+
case "dec": return { count: state.count - 1 };
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const engine = new TransactionalReducer(reducer, { count: 0 });
|
|
37
|
+
|
|
38
|
+
// 普通 dispatch —— 不可回滚
|
|
39
|
+
engine.dispatch({ type: "inc" });
|
|
40
|
+
console.log(engine.state); // { count: 1 }
|
|
41
|
+
|
|
42
|
+
// 事务性 dispatch —— 可回滚
|
|
43
|
+
engine.run(async (tx) => {
|
|
44
|
+
tx.dispatch({ type: "inc" }); // 乐观更新
|
|
45
|
+
await fetch("/api/inc"); // 异步请求
|
|
46
|
+
// 成功 → 自动 commit;失败 → 自动 rollback
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// 手动管理生命周期
|
|
50
|
+
const tx = engine.create();
|
|
51
|
+
tx.dispatch({ type: "inc" });
|
|
52
|
+
tx.rollback();
|
|
53
|
+
console.log(engine.state); // { count: 1 }(回滚了)
|
|
54
|
+
|
|
55
|
+
// 订阅状态变化
|
|
56
|
+
engine.subscribe((state) => {
|
|
57
|
+
console.log("state changed:", state);
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## API 参考
|
|
64
|
+
|
|
65
|
+
### 导出
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
TransactionalReducer,
|
|
70
|
+
type Transaction,
|
|
71
|
+
type TransactionHandle,
|
|
72
|
+
type TransactionOptions,
|
|
73
|
+
type TransactionalReducerOptions,
|
|
74
|
+
type OnErrorStrategy,
|
|
75
|
+
type OnDuplicateStrategy,
|
|
76
|
+
type ActionLogEntry,
|
|
77
|
+
type Ref,
|
|
78
|
+
} from "@transactional-reducer/core";
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### TransactionalReducer
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
class TransactionalReducer<S, A> {
|
|
85
|
+
constructor(reducer: (state: S, action: A) => S, initialState: S, options?: TransactionalReducerOptions<S>);
|
|
86
|
+
|
|
87
|
+
get state(): S;
|
|
88
|
+
subscribe(listener: (state: S) => void): () => void;
|
|
89
|
+
|
|
90
|
+
dispatch(action: A): void;
|
|
91
|
+
run<R>(task: (tx: TransactionHandle<A>) => R, options?: TransactionOptions): R;
|
|
92
|
+
create(options?: TransactionOptions): TransactionHandle<A>;
|
|
93
|
+
getTransaction(id: string): TransactionHandle<A> | undefined;
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `engine.state`
|
|
98
|
+
|
|
99
|
+
当前状态。每次 dispatch 后立即更新。
|
|
100
|
+
|
|
101
|
+
#### `engine.subscribe(listener)`
|
|
102
|
+
|
|
103
|
+
订阅状态变化。返回取消订阅的函数。
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const unsubscribe = engine.subscribe((state) => {
|
|
107
|
+
console.log(state);
|
|
108
|
+
});
|
|
109
|
+
unsubscribe(); // 取消订阅
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### `engine.dispatch(action)`
|
|
113
|
+
|
|
114
|
+
普通 dispatch,不可回滚。当没有活跃事务时,不会记录到 action log(不可能回滚,日志纯属开销)。当有活跃事务时,会记录到 action log 以确保回滚重放时保留。
|
|
115
|
+
|
|
116
|
+
#### `engine.run(task, options?)`
|
|
117
|
+
|
|
118
|
+
启动一个根事务并自动管理生命周期:
|
|
119
|
+
|
|
120
|
+
- **同步任务成功** → 先回滚所有仍活跃的子事务,再提交
|
|
121
|
+
- **同步任务抛错** → 根据 `onError` 决定回滚或提交
|
|
122
|
+
- **异步任务成功** → Promise resolve 后先回滚所有仍活跃的子事务,再提交
|
|
123
|
+
- **异步任务抛错** → Promise reject 后根据 `onError` 决定回滚或提交
|
|
124
|
+
|
|
125
|
+
`task` 的返回值会被原样返回(包括 Promise),方便链式调用。
|
|
126
|
+
|
|
127
|
+
> **注意**:`run`/`spawn` 在提交前会自动回滚所有仍活跃的子事务。这意味着如果父事务先完成,尚未结束的子事务会被强制回滚。这与手动调用 `tx.commit()` 的行为不同——手动 `commit()` 不会自动回滚活跃子事务。
|
|
128
|
+
|
|
129
|
+
#### `engine.create(options?)`
|
|
130
|
+
|
|
131
|
+
手动创建根事务。你需要自行调用 `tx.commit()` 或 `tx.rollback()` 来结束事务。
|
|
132
|
+
|
|
133
|
+
> **与 `run` 的区别**:
|
|
134
|
+
> - `create` 不提供自动生命周期管理(不会在成功/失败时自动提交/回滚)
|
|
135
|
+
> - `create` 不会在提交前自动回滚活跃子事务
|
|
136
|
+
> - `create` 支持所有去重策略,包括 `reuse`(返回旧事务句柄)
|
|
137
|
+
> - 在异步场景中,建议在 `commit()`/`rollback()` 前手动检查 `tx.isStale()`
|
|
138
|
+
|
|
139
|
+
#### `engine.getTransaction(id)`
|
|
140
|
+
|
|
141
|
+
按 id 查找事务。返回 `TransactionHandle` 或 `undefined`。
|
|
142
|
+
|
|
143
|
+
### TransactionOptions
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
interface TransactionOptions {
|
|
147
|
+
id?: string; // 事务 id,用于去重和查找
|
|
148
|
+
onError?: OnErrorStrategy; // "rollback" | "commit",默认 "rollback"
|
|
149
|
+
onDuplicate?: OnDuplicateStrategy; // "rollback" | "reuse" | "commit" | "reject",默认 "rollback"
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### TransactionHandle
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
interface TransactionHandle<A> {
|
|
157
|
+
readonly id: string;
|
|
158
|
+
readonly parentId: string | null;
|
|
159
|
+
readonly onError: OnErrorStrategy;
|
|
160
|
+
|
|
161
|
+
dispatch(action: A): void;
|
|
162
|
+
spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: TransactionOptions): R;
|
|
163
|
+
commit(): void;
|
|
164
|
+
rollback(): void;
|
|
165
|
+
isStale(): boolean;
|
|
166
|
+
onCancel(callback: () => void): void;
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
#### `tx.dispatch(action)`
|
|
171
|
+
|
|
172
|
+
在事务内派发 action。如果事务已过期,静默忽略。
|
|
173
|
+
|
|
174
|
+
#### `tx.spawn(task, options?)`
|
|
175
|
+
|
|
176
|
+
创建子事务并自动管理生命周期(同 `run`)。如果父事务已过期,抛出错误。
|
|
177
|
+
|
|
178
|
+
子事务的 `id` 不会自动拼接父事务 id,由用户完全控制,需注意避免冲突。
|
|
179
|
+
|
|
180
|
+
#### `tx.commit()`
|
|
181
|
+
|
|
182
|
+
提交事务。如果事务已过期,静默忽略。
|
|
183
|
+
|
|
184
|
+
- **子事务提交**:仅标记为 `"committed"`,仍在父事务范围内。父事务回滚也会撤销已提交子事务的变更。
|
|
185
|
+
- **根事务提交**:将 action 永久化,清理事务记录。
|
|
186
|
+
|
|
187
|
+
#### `tx.rollback()`
|
|
188
|
+
|
|
189
|
+
回滚事务。如果事务已过期,静默忽略。参见[回滚算法](#回滚算法的六个阶段)。
|
|
190
|
+
|
|
191
|
+
#### `tx.isStale()`
|
|
192
|
+
|
|
193
|
+
检查句柄是否过期。过期条件:`transactionsRef` 中该 id 持有不同对象,或句柄 status 不再 `"active"`。
|
|
194
|
+
|
|
195
|
+
#### `tx.onCancel(callback)`
|
|
196
|
+
|
|
197
|
+
注册取消回调。如果事务已过期,回调立即执行。参见 [onCancel 触发时机](#oncancel-触发时机)。
|
|
198
|
+
|
|
199
|
+
### TransactionalReducerOptions
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
interface TransactionalReducerOptions<S> {
|
|
203
|
+
idGenerator?: () => string; // 自定义 id 生成器
|
|
204
|
+
snapshot?: (state: S) => S; // 自定义快照函数(默认 structuredClone)
|
|
205
|
+
onDuplicate?: OnDuplicateStrategy; // 全局去重策略默认值(默认 "rollback")
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### OnErrorStrategy
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
type OnErrorStrategy = "rollback" | "commit"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- `"rollback"`:任务抛错时回滚事务(默认)
|
|
216
|
+
- `"commit"`:任务抛错时保留变更(提交边界)
|
|
217
|
+
|
|
218
|
+
### OnDuplicateStrategy
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
type OnDuplicateStrategy = "rollback" | "reuse" | "commit" | "reject"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
参见[去重策略](#去重--onduplicate-策略)。
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## 使用指南
|
|
229
|
+
|
|
230
|
+
### 1. 普通 Dispatch
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
engine.dispatch({ type: "inc" }); // 不可回滚
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### 2. 乐观更新 + 自动回滚
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
await engine.run(async (tx) => {
|
|
240
|
+
tx.dispatch({ type: "setSaving", value: true });
|
|
241
|
+
tx.dispatch({ type: "updateData", value: newData });
|
|
242
|
+
await saveToServer(newData);
|
|
243
|
+
// 成功 → 自动 commit
|
|
244
|
+
// 失败 → 自动 rollback
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### 3. 可取消的异步任务 + onCancel
|
|
249
|
+
|
|
250
|
+
给事务指定 `id`,相同 id 的新事务会自动取消(回滚)旧事务:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
async function handleSearch(query: string) {
|
|
254
|
+
await engine.run(async (tx) => {
|
|
255
|
+
const ac = new AbortController();
|
|
256
|
+
tx.onCancel(() => ac.abort());
|
|
257
|
+
tx.dispatch({ type: "setLoading", value: true });
|
|
258
|
+
const results = await fetchResults(query, { signal: ac.signal });
|
|
259
|
+
tx.dispatch({ type: "setResults", value: results });
|
|
260
|
+
}, { id: "search" });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 用户快速输入 "a"、"ab"、"abc":
|
|
264
|
+
// - "a" 和 "ab" 的请求被自动回滚
|
|
265
|
+
// - 只有 "abc" 的结果保留
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### 4. 手动管理事务生命周期
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
const tx = engine.create({ id: "edit-form" });
|
|
272
|
+
tx.dispatch({ type: "updateField", field: "name", value: "new" });
|
|
273
|
+
|
|
274
|
+
// 保存
|
|
275
|
+
await saveToServer(engine.state);
|
|
276
|
+
tx.commit();
|
|
277
|
+
|
|
278
|
+
// 或取消
|
|
279
|
+
tx.rollback();
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### 5. 嵌套事务(spawn)
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
await engine.run(async (tx) => {
|
|
286
|
+
tx.dispatch({ type: "setSubmitting", value: true });
|
|
287
|
+
|
|
288
|
+
await tx.spawn(async (childTx) => {
|
|
289
|
+
childTx.dispatch({ type: "setValidating", value: true });
|
|
290
|
+
const isValid = await validateForm();
|
|
291
|
+
if (!isValid) throw new Error("validation failed");
|
|
292
|
+
}, { id: "validate" });
|
|
293
|
+
|
|
294
|
+
await submitForm();
|
|
295
|
+
}, { id: "submit" });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
关键行为:
|
|
299
|
+
|
|
300
|
+
- 子事务提交后仍在父事务范围内——父事务回滚也会撤销子事务
|
|
301
|
+
- 子事务回滚不影响父事务
|
|
302
|
+
- 子事务 id 由用户指定,不会自动拼接
|
|
303
|
+
|
|
304
|
+
### 6. 提交边界(onError: "commit")
|
|
305
|
+
|
|
306
|
+
`onError: "commit"` 创建提交边界——父事务回滚时保留该子事务:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
await engine.run(async (tx) => {
|
|
310
|
+
await tx.spawn(async (childTx) => {
|
|
311
|
+
childTx.dispatch({ type: "updateCache", value: data });
|
|
312
|
+
}, { id: "local-cache", onError: "commit" });
|
|
313
|
+
|
|
314
|
+
await submitToServer(); // 失败 → 整个事务 rollback
|
|
315
|
+
// 但 local-cache 的变更被保留
|
|
316
|
+
}, { id: "submit" });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
提交边界的语义:
|
|
320
|
+
|
|
321
|
+
- 父事务回滚时,保留的子事务变为独立根事务(`parentId` 设为 `null`)
|
|
322
|
+
- 提交边界覆盖整个子树
|
|
323
|
+
- 保留的子事务可以继续操作
|
|
324
|
+
|
|
325
|
+
### 7. 混合 onError 策略
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
await engine.run(async (tx) => {
|
|
329
|
+
tx.dispatch({ type: "setOrderStatus", value: "pending" });
|
|
330
|
+
|
|
331
|
+
await tx.spawn(async (childTx) => {
|
|
332
|
+
childTx.dispatch({ type: "removeFromCart", itemId });
|
|
333
|
+
}, { id: "cart-update", onError: "commit" });
|
|
334
|
+
|
|
335
|
+
await tx.spawn(async (childTx) => {
|
|
336
|
+
childTx.dispatch({ type: "showSpinner", value: true });
|
|
337
|
+
}, { id: "ui-effects", onError: "rollback" });
|
|
338
|
+
|
|
339
|
+
await placeOrder();
|
|
340
|
+
}, { id: "order" });
|
|
341
|
+
// placeOrder() 失败:
|
|
342
|
+
// - cart-update 保留
|
|
343
|
+
// - ui-effects 回滚
|
|
344
|
+
// - tx 自身回滚
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### 8. 并发事务
|
|
348
|
+
|
|
349
|
+
```ts
|
|
350
|
+
const [result1, result2] = await Promise.all([
|
|
351
|
+
engine.run(async (tx) => {
|
|
352
|
+
tx.dispatch({ type: "setUsersLoading", value: true });
|
|
353
|
+
const users = await fetchUsers();
|
|
354
|
+
tx.dispatch({ type: "setUsers", value: users });
|
|
355
|
+
}, { id: "fetch-users" }),
|
|
356
|
+
engine.run(async (tx) => {
|
|
357
|
+
tx.dispatch({ type: "setPostsLoading", value: true });
|
|
358
|
+
const posts = await fetchPosts();
|
|
359
|
+
tx.dispatch({ type: "setPosts", value: posts });
|
|
360
|
+
}, { id: "fetch-posts" }),
|
|
361
|
+
]);
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
每个事务独立管理,回滚其中一个不影响另一个。
|
|
365
|
+
|
|
366
|
+
### 9. 状态订阅
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
const unsubscribe = engine.subscribe((state) => {
|
|
370
|
+
render(state);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// 在事务内每次 dispatch 都会触发通知
|
|
374
|
+
engine.run((tx) => {
|
|
375
|
+
tx.dispatch({ type: "inc" }); // 触发通知
|
|
376
|
+
tx.dispatch({ type: "inc" }); // 触发通知
|
|
377
|
+
tx.rollback(); // 触发通知(恢复状态)
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 10. 自定义快照函数
|
|
382
|
+
|
|
383
|
+
默认使用 `structuredClone`。如果状态包含不可克隆对象:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
const engine = new TransactionalReducer(reducer, initialState, {
|
|
387
|
+
snapshot: (state) => ({
|
|
388
|
+
...state,
|
|
389
|
+
data: [...state.data],
|
|
390
|
+
ref: state.ref,
|
|
391
|
+
}),
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### 11. 自定义 ID 生成器
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
let counter = 0;
|
|
399
|
+
const engine = new TransactionalReducer(reducer, initialState, {
|
|
400
|
+
idGenerator: () => `tx_${++counter}`,
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 核心机制详解
|
|
407
|
+
|
|
408
|
+
### Action Log + Snapshot + Replay
|
|
409
|
+
|
|
410
|
+
事务回滚不是简单的"恢复快照",而是**快照 + 重放**:
|
|
411
|
+
|
|
412
|
+
1. 事务创建时,记录当前状态的快照(snapshot)和 action log 的起始位置(snapshotIndex)
|
|
413
|
+
2. 事务内的每次 dispatch 都记录到 action log 中,附带 `txId` 标识
|
|
414
|
+
3. 回滚时,从快照开始重放所有 action log 条目,**跳过属于回滚事务的条目**
|
|
415
|
+
|
|
416
|
+
这种设计确保回滚仅撤销目标事务的变更,同时保留:
|
|
417
|
+
|
|
418
|
+
- 事务期间发生的普通(非事务)dispatch
|
|
419
|
+
- 并发兄弟事务的 action
|
|
420
|
+
- `onError: "commit"` 的后代事务的 action
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
时间线:
|
|
424
|
+
┌─ snapshot ─┬─── tx1 dispatch ────┬─── 普通 dispatch ────┬─── tx2 dispatch ────┐
|
|
425
|
+
│ │ inc │ inc │ dec │
|
|
426
|
+
└────────────┴──────────────────────┴───────────────────────┴──────────────────────┘
|
|
427
|
+
|
|
428
|
+
tx1 rollback → 从 snapshot 重放,跳过 tx1 的 inc,保留普通 dispatch 的 inc 和 tx2 的 dec
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### Generation 机制与过期句柄
|
|
432
|
+
|
|
433
|
+
当相同 id 的事务被替换时(去重机制),旧句柄变为"过期"。过期检测通过 **generation** 实现:
|
|
434
|
+
|
|
435
|
+
1. 每次用相同 id 创建新事务时,`generationRef` 中该 id 的 generation 递增
|
|
436
|
+
2. 旧句柄闭包绑定的 generation 不再匹配 `generationRef` 中的新值
|
|
437
|
+
3. `isStale()` 检查两个条件:`transactionsRef` 中该 id 是否持有不同对象,或句柄的 status 是否不再是 `"active"`
|
|
438
|
+
|
|
439
|
+
过期句柄的操作行为:
|
|
440
|
+
|
|
441
|
+
- `dispatch` → 忽略
|
|
442
|
+
- `commit` / `rollback` → 忽略
|
|
443
|
+
- `spawn` → 抛出错误
|
|
444
|
+
- `onCancel` → 立即执行回调
|
|
445
|
+
|
|
446
|
+
这防止了异步回调在过期句柄上误操作(例如,旧的搜索请求完成后不会覆盖新的搜索结果)。
|
|
447
|
+
|
|
448
|
+
### 去重 / onDuplicate 策略
|
|
449
|
+
|
|
450
|
+
当创建事务时指定了 `id`,如果相同 id 的活跃事务已存在,会根据 `onDuplicate` 策略处理:
|
|
451
|
+
|
|
452
|
+
| 策略 | 行为 | 适用场景 |
|
|
453
|
+
|------|------|----------|
|
|
454
|
+
| `rollback`(默认) | 回滚旧事务,创建新的 | 搜索/验证——新请求取代旧请求 |
|
|
455
|
+
| `commit` | 提交旧事务(含回滚其活跃子事务),创建新的 | 旧任务视为已完成 |
|
|
456
|
+
| `reuse` | `create`:返回旧句柄;`run`/`spawn`:抛错 | 编辑表单——只允许一个实例 |
|
|
457
|
+
| `reject` | 抛错,拒绝创建 | 严格禁止并发 |
|
|
458
|
+
|
|
459
|
+
策略优先级:`TransactionOptions.onDuplicate` > `TransactionalReducerOptions.onDuplicate` > `"rollback"`
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
// rollback(默认行为)——新请求取代旧请求
|
|
463
|
+
engine.run(async (tx) => { ... }, { id: "search" });
|
|
464
|
+
|
|
465
|
+
// reuse——只允许一个实例,复用已有事务
|
|
466
|
+
const tx = engine.create({ id: "edit-form", onDuplicate: "reuse" });
|
|
467
|
+
|
|
468
|
+
// reject——严格禁止并发
|
|
469
|
+
engine.run(async (tx) => { ... }, { id: "save", onDuplicate: "reject" });
|
|
470
|
+
|
|
471
|
+
// commit——旧任务视为已完成
|
|
472
|
+
engine.run(async (tx) => { ... }, { id: "refresh", onDuplicate: "commit" });
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
> **注意**:`reuse` 对 `run` 和 `spawn` 无效——它们会抛错而非复用旧事务,因为 `run`/`spawn` 的自动生命周期管理无法安全地应用于已有事务。
|
|
476
|
+
|
|
477
|
+
### onCancel 触发时机
|
|
478
|
+
|
|
479
|
+
- 事务被去重替换(相同 id 的新事务回滚旧事务)→ 触发
|
|
480
|
+
- 事务被手动 `rollback()` → 触发
|
|
481
|
+
- 事务因父事务回滚而被回滚(在 rollbackSet 中)→ 触发
|
|
482
|
+
- 事务因父事务自动提交而被强制回滚(`run`/`spawn` 完成时回滚仍活跃的子事务)→ 触发
|
|
483
|
+
- 事务被 `commit()` → **不触发**
|
|
484
|
+
- `onError: "commit"` 的子事务在父事务回滚时被保留 → **不触发**
|
|
485
|
+
|
|
486
|
+
特殊行为:
|
|
487
|
+
|
|
488
|
+
- 如果事务已过期(`isStale()` 返回 true),回调立即执行
|
|
489
|
+
- 可以注册多个回调,依次执行
|
|
490
|
+
- 回调不会双重触发
|
|
491
|
+
|
|
492
|
+
### 子事务提交 vs 根事务提交
|
|
493
|
+
|
|
494
|
+
**子事务提交**:仅将 status 标记为 `"committed"`。记录保留在 `transactionsRef` 中,父事务仍可管理它。父事务回滚也会撤销已提交子事务的变更。
|
|
495
|
+
|
|
496
|
+
**根事务提交**:
|
|
497
|
+
|
|
498
|
+
1. 将此事务及其后代的 action log 条目重新标记为普通 dispatch(`txId: null`),使其永久化
|
|
499
|
+
2. 从 `transactionsRef` 中删除根事务记录
|
|
500
|
+
3. 清理已提交的后代记录
|
|
501
|
+
4. 如果没有活跃事务剩余,清空整个 action log 和事务映射
|
|
502
|
+
|
|
503
|
+
### 回滚算法的六个阶段
|
|
504
|
+
|
|
505
|
+
若事务句柄已过期,`_rollback()` 立即返回。以下仅描述句柄仍活跃时的行为:
|
|
506
|
+
|
|
507
|
+
1. **分类后代**:将后代分为 `preserveSet`(`onError: "commit"` 的子树)和 `rollbackSet`
|
|
508
|
+
2. **标记 skipped**:将 `rollbackSet` 的 action 标记为 `skipped`
|
|
509
|
+
3. **重新标记 preserveSet**:将已提交的保留子事务的 action 重新标记为普通 dispatch(`txId: null`)
|
|
510
|
+
4. **重放**:从快照重放,跳过 `skipped` 条目
|
|
511
|
+
5. **分离保留的事务**:已提交的保留事务被删除;活跃的保留事务 `parentId` 设为 `null`(变为独立根),并更新其 snapshot
|
|
512
|
+
6. **最终清理**:若无活跃事务剩余,清空所有数据
|
|
513
|
+
|
|
514
|
+
### 自动清理
|
|
515
|
+
|
|
516
|
+
当没有活跃事务时,action log、事务映射和 generation 映射会被清空。因为不可能再发生回滚,日志纯属开销。
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
## 常见场景
|
|
521
|
+
|
|
522
|
+
### 搜索自动取消
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
const handleSearch = debounce(async (query: string) => {
|
|
526
|
+
await engine.run(async (tx) => {
|
|
527
|
+
const ac = new AbortController();
|
|
528
|
+
tx.onCancel(() => ac.abort());
|
|
529
|
+
tx.dispatch({ type: "setQuery", value: query });
|
|
530
|
+
tx.dispatch({ type: "setLoading", value: true });
|
|
531
|
+
const results = await searchAPI(query, { signal: ac.signal });
|
|
532
|
+
tx.dispatch({ type: "setResults", value: results });
|
|
533
|
+
tx.dispatch({ type: "setLoading", value: false });
|
|
534
|
+
}, { id: "search" });
|
|
535
|
+
}, 300);
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### 表单编辑 + 取消恢复
|
|
539
|
+
|
|
540
|
+
```ts
|
|
541
|
+
const tx = engine.create({ id: "edit-profile" });
|
|
542
|
+
|
|
543
|
+
function updateField(field: string, value: string) {
|
|
544
|
+
tx.dispatch({ type: "updateField", field, value });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function save() {
|
|
548
|
+
try {
|
|
549
|
+
await saveProfile(engine.state);
|
|
550
|
+
tx.commit();
|
|
551
|
+
} catch {
|
|
552
|
+
tx.rollback();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function cancel() {
|
|
557
|
+
tx.rollback();
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
### 多步骤提交 + 部分保留
|
|
562
|
+
|
|
563
|
+
```ts
|
|
564
|
+
await engine.run(async (tx) => {
|
|
565
|
+
await tx.spawn(async (childTx) => {
|
|
566
|
+
childTx.dispatch({ type: "lockItems", items });
|
|
567
|
+
await lockInventory(items);
|
|
568
|
+
}, { id: "lock-inventory", onError: "commit" });
|
|
569
|
+
|
|
570
|
+
tx.dispatch({ type: "setPaymentProcessing", value: true });
|
|
571
|
+
await processPayment(paymentInfo);
|
|
572
|
+
tx.dispatch({ type: "setPaymentProcessing", value: false });
|
|
573
|
+
}, { id: "checkout" });
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
## 注意事项
|
|
579
|
+
|
|
580
|
+
1. **事务 id 冲突**:子事务的 id 不会自动拼接父事务 id,由用户完全控制。需注意避免不同父事务下的子事务使用相同 id。
|
|
581
|
+
|
|
582
|
+
2. **过期句柄安全**:对过期句柄的 `dispatch`/`commit`/`rollback` 会被静默忽略,`spawn` 会抛出错误。这是设计行为,防止异步回调干扰新事务。
|
|
583
|
+
|
|
584
|
+
3. **快照性能**:默认使用 `structuredClone`,对大型状态对象可能有性能开销。可通过 `snapshot` 选项提供更轻量的克隆函数。
|
|
585
|
+
|
|
586
|
+
4. **幂等性要求**:由于回滚使用"快照 + 重放"机制,reducer 应尽量保持幂等性——相同 action 在不同基础状态上应产生合理的结果。
|
|
587
|
+
|
|
588
|
+
5. **同步 vs 异步**:`run` 和 `spawn` 对同步任务和异步任务的生命周期管理略有不同。同步任务执行期间不可能过期,无需额外检查;异步任务的 Promise 回调中会检查过期状态。
|
|
589
|
+
|
|
590
|
+
6. **onCancel 与 AbortError**:使用 `onCancel` + `AbortController` 取消异步请求后,被取消事务的 Promise 会以 `AbortError` reject(而非静默跳过 commit 后 resolve)。这是预期行为——取消意味着任务中止,错误应传播给调用方。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transactional-reducer/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.1",
|
|
4
4
|
"description": "A transactional state management library with reducer pattern",
|
|
5
5
|
"main": "dist/TransactionalReducer.js",
|
|
6
6
|
"types": "dist/TransactionalReducer.d.ts",
|
|
@@ -20,26 +20,28 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
|
-
"scripts": {
|
|
24
|
-
"test": "vitest --run",
|
|
25
|
-
"build": "tsc -p tsconfig.build.json",
|
|
26
|
-
"prepublishOnly": "pnpm run build"
|
|
27
|
-
},
|
|
28
23
|
"keywords": [
|
|
29
24
|
"transaction",
|
|
30
25
|
"reducer",
|
|
31
26
|
"state-management"
|
|
32
27
|
],
|
|
33
|
-
"author": "",
|
|
34
|
-
"license": "
|
|
28
|
+
"author": "anuoua <anuoua@gmail.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"homepage": "https://github.com/anuoua/transactional-reducer#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/anuoua/transactional-reducer/issues"
|
|
33
|
+
},
|
|
35
34
|
"repository": {
|
|
36
35
|
"type": "git",
|
|
37
36
|
"url": "git+https://github.com/anuoua/transactional-reducer.git",
|
|
38
37
|
"directory": "packages/core"
|
|
39
38
|
},
|
|
40
|
-
"packageManager": "pnpm@10.32.1",
|
|
41
39
|
"devDependencies": {
|
|
42
40
|
"typescript": "^6.0.3",
|
|
43
41
|
"vitest": "^4.1.7"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest --run",
|
|
45
|
+
"build": "tsc -p tsconfig.build.json"
|
|
44
46
|
}
|
|
45
|
-
}
|
|
47
|
+
}
|