@transactional-reducer/core 0.0.1 → 0.0.2
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/dist/index.d.mts +95 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +359 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +5 -16
- package/dist/Transaction.d.ts +0 -72
- package/dist/Transaction.d.ts.map +0 -1
- package/dist/Transaction.js +0 -408
- package/dist/Transaction.js.map +0 -1
- package/dist/TransactionalReducer.d.ts +0 -26
- package/dist/TransactionalReducer.d.ts.map +0 -1
- package/dist/TransactionalReducer.js +0 -208
- package/dist/TransactionalReducer.js.map +0 -1
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/Transaction.d.ts
|
|
2
|
+
interface Ref<T> {
|
|
3
|
+
current: T;
|
|
4
|
+
}
|
|
5
|
+
interface ActionLogEntry<A> {
|
|
6
|
+
action: A;
|
|
7
|
+
txId: string | null;
|
|
8
|
+
generation: number;
|
|
9
|
+
skipped?: boolean;
|
|
10
|
+
}
|
|
11
|
+
type OnErrorStrategy = "rollback" | "commit";
|
|
12
|
+
type OnDuplicateStrategy = "rollback" | "reuse" | "commit" | "reject";
|
|
13
|
+
interface TransactionOptions {
|
|
14
|
+
id?: string;
|
|
15
|
+
onError?: OnErrorStrategy;
|
|
16
|
+
onDuplicate?: OnDuplicateStrategy;
|
|
17
|
+
}
|
|
18
|
+
type SpawnOptions = TransactionOptions;
|
|
19
|
+
interface TransactionHandle<A> {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly parentId: string | null;
|
|
22
|
+
readonly onError: OnErrorStrategy;
|
|
23
|
+
dispatch(action: A): void;
|
|
24
|
+
spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R;
|
|
25
|
+
commit(): void;
|
|
26
|
+
rollback(): void;
|
|
27
|
+
isStale(): boolean;
|
|
28
|
+
onCancel(callback: () => void): void;
|
|
29
|
+
}
|
|
30
|
+
interface TransactionalReducerOptions<S = any> {
|
|
31
|
+
idGenerator?: () => string;
|
|
32
|
+
snapshot?: (state: S) => S;
|
|
33
|
+
onDuplicate?: OnDuplicateStrategy;
|
|
34
|
+
}
|
|
35
|
+
interface TransactionEngine<S, A> {
|
|
36
|
+
readonly reducer: (state: S, action: A) => S;
|
|
37
|
+
readonly options: TransactionalReducerOptions<S> | undefined;
|
|
38
|
+
readonly stateRef: Ref<S>;
|
|
39
|
+
readonly actionLogRef: Ref<ActionLogEntry<A>[]>;
|
|
40
|
+
readonly transactionsRef: Ref<Map<string, Transaction<S, A>>>;
|
|
41
|
+
readonly generationRef: Ref<Map<string, number>>;
|
|
42
|
+
_createTx(id: string | undefined, parentId: string | null, onError: OnErrorStrategy, onDuplicate: OnDuplicateStrategy): Transaction<S, A>;
|
|
43
|
+
_runWithTx<R>(tx: Transaction<S, A>, task: (tx: TransactionHandle<A>) => R): R;
|
|
44
|
+
_applyAction(action: A): void;
|
|
45
|
+
_notify(): void;
|
|
46
|
+
}
|
|
47
|
+
declare class Transaction<S, A> implements TransactionHandle<A> {
|
|
48
|
+
readonly id: string;
|
|
49
|
+
parentId: string | null;
|
|
50
|
+
readonly onError: OnErrorStrategy;
|
|
51
|
+
readonly generation: number;
|
|
52
|
+
snapshot: S;
|
|
53
|
+
readonly snapshotIndex: number;
|
|
54
|
+
status: "active" | "committed" | "rolledback";
|
|
55
|
+
cancelCallbacks: (() => void)[];
|
|
56
|
+
private engine;
|
|
57
|
+
constructor(engine: TransactionEngine<S, A>, id: string, parentId: string | null, onError: OnErrorStrategy, generation: number, snapshot: S, snapshotIndex: number);
|
|
58
|
+
isStale(): boolean;
|
|
59
|
+
onCancel(callback: () => void): void;
|
|
60
|
+
dispatch(action: A): void;
|
|
61
|
+
spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R;
|
|
62
|
+
commit(): void;
|
|
63
|
+
rollback(): void;
|
|
64
|
+
_commit(): void;
|
|
65
|
+
_rollback(): void;
|
|
66
|
+
_rollbackActiveDescendants(): void;
|
|
67
|
+
private _hasActiveTransactions;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/TransactionalReducer.d.ts
|
|
71
|
+
declare class TransactionalReducer<S, A> implements TransactionEngine<S, A> {
|
|
72
|
+
readonly reducer: (state: S, action: A) => S;
|
|
73
|
+
readonly options: TransactionalReducerOptions<S> | undefined;
|
|
74
|
+
readonly stateRef: Ref<S>;
|
|
75
|
+
readonly actionLogRef: Ref<ActionLogEntry<A>[]>;
|
|
76
|
+
readonly transactionsRef: Ref<Map<string, Transaction<S, A>>>;
|
|
77
|
+
readonly generationRef: Ref<Map<string, number>>;
|
|
78
|
+
private _listeners;
|
|
79
|
+
constructor(reducer: (state: S, action: A) => S, initialState: S, options?: TransactionalReducerOptions<S>);
|
|
80
|
+
get state(): S;
|
|
81
|
+
subscribe(listener: (state: S) => void): () => void;
|
|
82
|
+
_notify(): void;
|
|
83
|
+
dispatch(action: A): void;
|
|
84
|
+
run<R>(task: (tx: TransactionHandle<A>) => R, options?: TransactionOptions): R;
|
|
85
|
+
create(options?: TransactionOptions): TransactionHandle<A>;
|
|
86
|
+
getTransaction(id: string): TransactionHandle<A> | undefined;
|
|
87
|
+
_applyAction(action: A): void;
|
|
88
|
+
_createTx(id: string | undefined, parentId: string | null, onError: OnErrorStrategy, onDuplicate: OnDuplicateStrategy): Transaction<S, A>;
|
|
89
|
+
_runWithTx<R>(tx: Transaction<S, A>, task: (tx: TransactionHandle<A>) => R): R;
|
|
90
|
+
private _nextGeneration;
|
|
91
|
+
private _hasActiveTransactions;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { type ActionLogEntry, type OnDuplicateStrategy, type OnErrorStrategy, type Ref, type Transaction, type TransactionHandle, type TransactionOptions, TransactionalReducer, type TransactionalReducerOptions };
|
|
95
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/Transaction.ts","../src/TransactionalReducer.ts"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
//#region src/Transaction.ts
|
|
2
|
+
function _generateId() {
|
|
3
|
+
return `tx_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
4
|
+
}
|
|
5
|
+
function _getAllDescendants(txId, transactions) {
|
|
6
|
+
const result = [];
|
|
7
|
+
for (const [, tx] of transactions) if (tx.parentId === txId) result.push(tx.id, ..._getAllDescendants(tx.id, transactions));
|
|
8
|
+
return result;
|
|
9
|
+
}
|
|
10
|
+
function _isDescendantOf(candidateTxId, ancestorTxId, transactions) {
|
|
11
|
+
if (candidateTxId === null) return false;
|
|
12
|
+
let current = candidateTxId;
|
|
13
|
+
while (current !== null) {
|
|
14
|
+
if (current === ancestorTxId) return true;
|
|
15
|
+
current = transactions.get(current)?.parentId ?? null;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function _cleanupCommittedDescendants(parentId, transactions) {
|
|
20
|
+
for (const [id, tx] of transactions) if (tx.parentId === parentId && tx.status === "committed") {
|
|
21
|
+
transactions.delete(id);
|
|
22
|
+
_cleanupCommittedDescendants(id, transactions);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var Transaction = class {
|
|
26
|
+
id;
|
|
27
|
+
parentId;
|
|
28
|
+
onError;
|
|
29
|
+
generation;
|
|
30
|
+
snapshot;
|
|
31
|
+
snapshotIndex;
|
|
32
|
+
status = "active";
|
|
33
|
+
cancelCallbacks = [];
|
|
34
|
+
engine;
|
|
35
|
+
constructor(engine, id, parentId, onError, generation, snapshot, snapshotIndex) {
|
|
36
|
+
this.engine = engine;
|
|
37
|
+
this.id = id;
|
|
38
|
+
this.parentId = parentId;
|
|
39
|
+
this.onError = onError;
|
|
40
|
+
this.generation = generation;
|
|
41
|
+
this.snapshot = snapshot;
|
|
42
|
+
this.snapshotIndex = snapshotIndex;
|
|
43
|
+
}
|
|
44
|
+
isStale() {
|
|
45
|
+
return this.engine.transactionsRef.current.get(this.id) !== this || this.status !== "active";
|
|
46
|
+
}
|
|
47
|
+
onCancel(callback) {
|
|
48
|
+
if (this.isStale()) {
|
|
49
|
+
callback();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
this.cancelCallbacks.push(callback);
|
|
53
|
+
}
|
|
54
|
+
dispatch(action) {
|
|
55
|
+
if (this.isStale()) return;
|
|
56
|
+
this.engine.actionLogRef.current.push({
|
|
57
|
+
action,
|
|
58
|
+
txId: this.id,
|
|
59
|
+
generation: this.generation
|
|
60
|
+
});
|
|
61
|
+
this.engine._applyAction(action);
|
|
62
|
+
}
|
|
63
|
+
spawn(task, options) {
|
|
64
|
+
if (this.engine.transactionsRef.current.get(this.id) !== this || this.status !== "active" || this.generation !== this.engine.generationRef.current.get(this.id)) throw new Error(`Cannot spawn from transaction "${this.id}": parent is no longer active`);
|
|
65
|
+
const childId = options?.id ?? _generateId();
|
|
66
|
+
const childOnError = options?.onError ?? "rollback";
|
|
67
|
+
const childOnDuplicate = options?.onDuplicate ?? this.engine.options?.onDuplicate ?? "rollback";
|
|
68
|
+
if (childOnDuplicate === "reuse" && options?.id) {
|
|
69
|
+
if (this.engine.transactionsRef.current.get(options.id)?.status === "active") throw new Error(`Cannot spawn: transaction "${options.id}" is already active`);
|
|
70
|
+
}
|
|
71
|
+
const childTx = this.engine._createTx(childId, this.id, childOnError, childOnDuplicate);
|
|
72
|
+
return this.engine._runWithTx(childTx, task);
|
|
73
|
+
}
|
|
74
|
+
commit() {
|
|
75
|
+
if (this.isStale()) return;
|
|
76
|
+
this._commit();
|
|
77
|
+
}
|
|
78
|
+
rollback() {
|
|
79
|
+
if (this.isStale()) return;
|
|
80
|
+
this._rollback();
|
|
81
|
+
}
|
|
82
|
+
_commit() {
|
|
83
|
+
if (this.parentId !== null) this.status = "committed";
|
|
84
|
+
else {
|
|
85
|
+
this.status = "committed";
|
|
86
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
87
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
88
|
+
if (entry.skipped) continue;
|
|
89
|
+
if (entry.txId === this.id || _isDescendantOf(entry.txId, this.id, this.engine.transactionsRef.current)) this.engine.actionLogRef.current[i] = {
|
|
90
|
+
action: entry.action,
|
|
91
|
+
txId: null,
|
|
92
|
+
generation: 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
this.engine.transactionsRef.current.delete(this.id);
|
|
96
|
+
_cleanupCommittedDescendants(this.id, this.engine.transactionsRef.current);
|
|
97
|
+
if (!this._hasActiveTransactions()) {
|
|
98
|
+
this.engine.actionLogRef.current = [];
|
|
99
|
+
this.engine.transactionsRef.current.clear();
|
|
100
|
+
this.engine.generationRef.current.clear();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
_rollback() {
|
|
105
|
+
if (this.isStale()) return;
|
|
106
|
+
const descendants = _getAllDescendants(this.id, this.engine.transactionsRef.current);
|
|
107
|
+
const preserveSet = /* @__PURE__ */ new Set();
|
|
108
|
+
const visited = /* @__PURE__ */ new Set();
|
|
109
|
+
for (const descId of descendants) {
|
|
110
|
+
if (visited.has(descId)) continue;
|
|
111
|
+
const descTx = this.engine.transactionsRef.current.get(descId);
|
|
112
|
+
if (descTx?.onError === "commit") {
|
|
113
|
+
let underPreserve = false;
|
|
114
|
+
let current = descTx.parentId;
|
|
115
|
+
while (current !== null && current !== this.id) {
|
|
116
|
+
if (preserveSet.has(current)) {
|
|
117
|
+
underPreserve = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
current = this.engine.transactionsRef.current.get(current)?.parentId ?? null;
|
|
121
|
+
}
|
|
122
|
+
if (!underPreserve) {
|
|
123
|
+
preserveSet.add(descId);
|
|
124
|
+
const subDescendants = _getAllDescendants(descId, this.engine.transactionsRef.current);
|
|
125
|
+
for (const subId of subDescendants) {
|
|
126
|
+
preserveSet.add(subId);
|
|
127
|
+
visited.add(subId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
visited.add(descId);
|
|
132
|
+
}
|
|
133
|
+
const rollbackSet = /* @__PURE__ */ new Set();
|
|
134
|
+
rollbackSet.add(this.id);
|
|
135
|
+
for (const descId of descendants) if (!preserveSet.has(descId)) rollbackSet.add(descId);
|
|
136
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
137
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
138
|
+
if (entry.txId !== null && rollbackSet.has(entry.txId)) entry.skipped = true;
|
|
139
|
+
}
|
|
140
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
141
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
142
|
+
if (!entry.skipped && entry.txId !== null && preserveSet.has(entry.txId)) {
|
|
143
|
+
if (this.engine.transactionsRef.current.get(entry.txId)?.status === "committed") this.engine.actionLogRef.current[i] = {
|
|
144
|
+
action: entry.action,
|
|
145
|
+
txId: null,
|
|
146
|
+
generation: 0
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let replayState = this.snapshot;
|
|
151
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
152
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
153
|
+
if (entry.skipped) continue;
|
|
154
|
+
replayState = this.engine.reducer(replayState, entry.action);
|
|
155
|
+
}
|
|
156
|
+
for (const id of rollbackSet) {
|
|
157
|
+
const record = this.engine.transactionsRef.current.get(id);
|
|
158
|
+
if (record) record.status = "rolledback";
|
|
159
|
+
}
|
|
160
|
+
this.engine.stateRef.current = replayState;
|
|
161
|
+
this.engine._notify();
|
|
162
|
+
for (const id of rollbackSet) {
|
|
163
|
+
const record = this.engine.transactionsRef.current.get(id);
|
|
164
|
+
if (record?.cancelCallbacks.length) {
|
|
165
|
+
const callbacks = [...record.cancelCallbacks];
|
|
166
|
+
record.cancelCallbacks = [];
|
|
167
|
+
for (const cb of callbacks) cb();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const id of rollbackSet) this.engine.transactionsRef.current.delete(id);
|
|
171
|
+
for (const id of preserveSet) {
|
|
172
|
+
const record = this.engine.transactionsRef.current.get(id);
|
|
173
|
+
if (record) {
|
|
174
|
+
if (record.status === "committed") this.engine.transactionsRef.current.delete(id);
|
|
175
|
+
else if (record.status === "active") {
|
|
176
|
+
if (record.parentId !== null && rollbackSet.has(record.parentId)) {
|
|
177
|
+
record.parentId = null;
|
|
178
|
+
const childDescendants = _getAllDescendants(id, this.engine.transactionsRef.current);
|
|
179
|
+
const childOwnSet = /* @__PURE__ */ new Set();
|
|
180
|
+
childOwnSet.add(id);
|
|
181
|
+
for (const descId of childDescendants) childOwnSet.add(descId);
|
|
182
|
+
let newSnapshot = this.snapshot;
|
|
183
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
184
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
185
|
+
if (entry.skipped) continue;
|
|
186
|
+
if (entry.txId !== null && (rollbackSet.has(entry.txId) || childOwnSet.has(entry.txId))) continue;
|
|
187
|
+
newSnapshot = this.engine.reducer(newSnapshot, entry.action);
|
|
188
|
+
}
|
|
189
|
+
record.snapshot = newSnapshot;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const id of preserveSet) if (this.engine.transactionsRef.current.get(id)?.status === "active") _cleanupCommittedDescendants(id, this.engine.transactionsRef.current);
|
|
195
|
+
if (!this._hasActiveTransactions()) {
|
|
196
|
+
this.engine.actionLogRef.current = [];
|
|
197
|
+
this.engine.transactionsRef.current.clear();
|
|
198
|
+
this.engine.generationRef.current.clear();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
_rollbackActiveDescendants() {
|
|
202
|
+
const activeChildren = [];
|
|
203
|
+
for (const [, tx] of this.engine.transactionsRef.current) if (tx.parentId === this.id && tx.status === "active") activeChildren.push(tx.id);
|
|
204
|
+
for (const id of activeChildren) {
|
|
205
|
+
const tx = this.engine.transactionsRef.current.get(id);
|
|
206
|
+
if (tx?.status === "active") tx._rollback();
|
|
207
|
+
}
|
|
208
|
+
let replayState = this.snapshot;
|
|
209
|
+
for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {
|
|
210
|
+
const entry = this.engine.actionLogRef.current[i];
|
|
211
|
+
if (entry.skipped) continue;
|
|
212
|
+
replayState = this.engine.reducer(replayState, entry.action);
|
|
213
|
+
}
|
|
214
|
+
this.engine.stateRef.current = replayState;
|
|
215
|
+
this.engine._notify();
|
|
216
|
+
}
|
|
217
|
+
_hasActiveTransactions() {
|
|
218
|
+
for (const tx of this.engine.transactionsRef.current.values()) if (tx.status === "active") return true;
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
223
|
+
//#region src/TransactionalReducer.ts
|
|
224
|
+
var TransactionalReducer = class {
|
|
225
|
+
reducer;
|
|
226
|
+
options;
|
|
227
|
+
stateRef;
|
|
228
|
+
actionLogRef;
|
|
229
|
+
transactionsRef;
|
|
230
|
+
generationRef;
|
|
231
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
232
|
+
constructor(reducer, initialState, options) {
|
|
233
|
+
this.reducer = reducer;
|
|
234
|
+
this.options = options;
|
|
235
|
+
this.stateRef = { current: initialState };
|
|
236
|
+
this.actionLogRef = { current: [] };
|
|
237
|
+
this.transactionsRef = { current: /* @__PURE__ */ new Map() };
|
|
238
|
+
this.generationRef = { current: /* @__PURE__ */ new Map() };
|
|
239
|
+
}
|
|
240
|
+
get state() {
|
|
241
|
+
return this.stateRef.current;
|
|
242
|
+
}
|
|
243
|
+
subscribe(listener) {
|
|
244
|
+
this._listeners.add(listener);
|
|
245
|
+
return () => {
|
|
246
|
+
this._listeners.delete(listener);
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
_notify() {
|
|
250
|
+
const state = this.stateRef.current;
|
|
251
|
+
for (const listener of this._listeners) listener(state);
|
|
252
|
+
}
|
|
253
|
+
dispatch(action) {
|
|
254
|
+
if (this._hasActiveTransactions()) this.actionLogRef.current.push({
|
|
255
|
+
action,
|
|
256
|
+
txId: null,
|
|
257
|
+
generation: 0
|
|
258
|
+
});
|
|
259
|
+
this._applyAction(action);
|
|
260
|
+
}
|
|
261
|
+
run(task, options) {
|
|
262
|
+
const strategy = options?.onDuplicate ?? this.options?.onDuplicate ?? "rollback";
|
|
263
|
+
if (strategy === "reuse" && options?.id) {
|
|
264
|
+
if (this.transactionsRef.current.get(options.id)?.status === "active") throw new Error(`Cannot run: transaction "${options.id}" is already active`);
|
|
265
|
+
}
|
|
266
|
+
const tx = this._createTx(options?.id, null, options?.onError ?? "rollback", strategy);
|
|
267
|
+
return this._runWithTx(tx, task);
|
|
268
|
+
}
|
|
269
|
+
create(options) {
|
|
270
|
+
const strategy = options?.onDuplicate ?? this.options?.onDuplicate ?? "rollback";
|
|
271
|
+
if (strategy === "reuse" && options?.id) {
|
|
272
|
+
const existing = this.transactionsRef.current.get(options.id);
|
|
273
|
+
if (existing?.status === "active") return existing;
|
|
274
|
+
}
|
|
275
|
+
return this._createTx(options?.id, null, options?.onError ?? "rollback", strategy);
|
|
276
|
+
}
|
|
277
|
+
getTransaction(id) {
|
|
278
|
+
return this.transactionsRef.current.get(id);
|
|
279
|
+
}
|
|
280
|
+
_applyAction(action) {
|
|
281
|
+
this.stateRef.current = this.reducer(this.stateRef.current, action);
|
|
282
|
+
this._notify();
|
|
283
|
+
}
|
|
284
|
+
_createTx(id, parentId, onError, onDuplicate) {
|
|
285
|
+
const txId = id || this.options?.idGenerator?.() || _generateId();
|
|
286
|
+
const existing = this.transactionsRef.current.get(txId);
|
|
287
|
+
if (existing?.status === "active") switch (onDuplicate) {
|
|
288
|
+
case "rollback":
|
|
289
|
+
existing._rollback();
|
|
290
|
+
break;
|
|
291
|
+
case "commit":
|
|
292
|
+
existing._rollbackActiveDescendants();
|
|
293
|
+
existing._commit();
|
|
294
|
+
if (existing.parentId !== null) {
|
|
295
|
+
for (let i = existing.snapshotIndex; i < this.actionLogRef.current.length; i++) {
|
|
296
|
+
const entry = this.actionLogRef.current[i];
|
|
297
|
+
if (entry.skipped) continue;
|
|
298
|
+
if (entry.txId === existing.id || _isDescendantOf(entry.txId, existing.id, this.transactionsRef.current)) this.actionLogRef.current[i] = {
|
|
299
|
+
action: entry.action,
|
|
300
|
+
txId: null,
|
|
301
|
+
generation: 0
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
_cleanupCommittedDescendants(existing.id, this.transactionsRef.current);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case "reject": throw new Error(`Transaction "${txId}" is already active`);
|
|
308
|
+
case "reuse": break;
|
|
309
|
+
}
|
|
310
|
+
const generation = this._nextGeneration(txId);
|
|
311
|
+
const snapshot = (this.options?.snapshot ?? structuredClone)(this.stateRef.current);
|
|
312
|
+
const snapshotIndex = this.actionLogRef.current.length;
|
|
313
|
+
const tx = new Transaction(this, txId, parentId, onError, generation, snapshot, snapshotIndex);
|
|
314
|
+
this.transactionsRef.current.set(txId, tx);
|
|
315
|
+
return tx;
|
|
316
|
+
}
|
|
317
|
+
_runWithTx(tx, task) {
|
|
318
|
+
try {
|
|
319
|
+
const result = task(tx);
|
|
320
|
+
if (result instanceof Promise) return result.then((r) => {
|
|
321
|
+
if (!tx.isStale()) {
|
|
322
|
+
tx._rollbackActiveDescendants();
|
|
323
|
+
tx._commit();
|
|
324
|
+
}
|
|
325
|
+
return r;
|
|
326
|
+
}, (e) => {
|
|
327
|
+
if (tx.onError === "commit") {
|
|
328
|
+
if (!tx.isStale()) {
|
|
329
|
+
tx._rollbackActiveDescendants();
|
|
330
|
+
tx._commit();
|
|
331
|
+
}
|
|
332
|
+
} else tx._rollback();
|
|
333
|
+
throw e;
|
|
334
|
+
});
|
|
335
|
+
tx._rollbackActiveDescendants();
|
|
336
|
+
tx._commit();
|
|
337
|
+
return result;
|
|
338
|
+
} catch (e) {
|
|
339
|
+
if (tx.onError === "commit") {
|
|
340
|
+
tx._rollbackActiveDescendants();
|
|
341
|
+
tx._commit();
|
|
342
|
+
} else tx._rollback();
|
|
343
|
+
throw e;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
_nextGeneration(txId) {
|
|
347
|
+
const next = (this.generationRef.current.get(txId) ?? 0) + 1;
|
|
348
|
+
this.generationRef.current.set(txId, next);
|
|
349
|
+
return next;
|
|
350
|
+
}
|
|
351
|
+
_hasActiveTransactions() {
|
|
352
|
+
for (const tx of this.transactionsRef.current.values()) if (tx.status === "active") return true;
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
//#endregion
|
|
357
|
+
export { TransactionalReducer };
|
|
358
|
+
|
|
359
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/Transaction.ts","../src/TransactionalReducer.ts"],"sourcesContent":["export interface Ref<T> {\n current: T;\n}\n\nexport interface ActionLogEntry<A> {\n action: A;\n // null = 普通 dispatch 或已提交的根事务 action;\n // string = 在该事务内派发的 action\n txId: string | null;\n // 每个 txId 单调递增。spawn() 用它检测过期句柄:\n // 当 createTx 用相同 id 替换事务时,新的 generation\n // 不会匹配旧句柄闭包绑定的 generation,使旧句柄变为过期。\n generation: number;\n // 在回滚重放期间标记为 true。被跳过的条目不会参与\n // 回滚后重建状态的重放过程。\n skipped?: boolean;\n}\n\nexport type OnErrorStrategy = \"rollback\" | \"commit\";\nexport type OnDuplicateStrategy = \"rollback\" | \"reuse\" | \"commit\" | \"reject\";\n\nexport interface TransactionOptions {\n id?: string;\n onError?: OnErrorStrategy;\n onDuplicate?: OnDuplicateStrategy;\n}\n\nexport type SpawnOptions = TransactionOptions;\n\nexport interface TransactionHandle<A> {\n readonly id: string;\n readonly parentId: string | null;\n readonly onError: OnErrorStrategy;\n dispatch(action: A): void;\n spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R;\n commit(): void;\n rollback(): void;\n isStale(): boolean;\n onCancel(callback: () => void): void;\n}\n\nexport interface TransactionalReducerOptions<S = any> {\n idGenerator?: () => string;\n snapshot?: (state: S) => S;\n onDuplicate?: OnDuplicateStrategy;\n}\n\n// Transaction 引擎接口,定义 Transaction 对引擎的依赖。\n// 由 TransactionalReducer 类实现。\nexport interface TransactionEngine<S, A> {\n readonly reducer: (state: S, action: A) => S;\n readonly options: TransactionalReducerOptions<S> | undefined;\n readonly stateRef: Ref<S>;\n readonly actionLogRef: Ref<ActionLogEntry<A>[]>;\n readonly transactionsRef: Ref<Map<string, Transaction<S, A>>>;\n readonly generationRef: Ref<Map<string, number>>;\n _createTx(\n id: string | undefined,\n parentId: string | null,\n onError: OnErrorStrategy,\n onDuplicate: OnDuplicateStrategy,\n ): Transaction<S, A>;\n _runWithTx<R>(tx: Transaction<S, A>, task: (tx: TransactionHandle<A>) => R): R;\n _applyAction(action: A): void;\n _notify(): void;\n}\n\nexport function _generateId(): string {\n return `tx_${Date.now()}_${Math.random().toString(36).slice(2)}`;\n}\n\nexport function _getAllDescendants<S>(\n txId: string,\n transactions: Map<string, Transaction<S, any>>,\n): string[] {\n const result: string[] = [];\n for (const [, tx] of transactions) {\n if (tx.parentId === txId) {\n result.push(tx.id, ..._getAllDescendants(tx.id, transactions));\n }\n }\n return result;\n}\n\nexport function _isDescendantOf<S>(\n candidateTxId: string | null,\n ancestorTxId: string,\n transactions: Map<string, Transaction<S, any>>,\n): boolean {\n if (candidateTxId === null) return false;\n let current: string | null = candidateTxId;\n while (current !== null) {\n if (current === ancestorTxId) return true;\n const record = transactions.get(current);\n current = record?.parentId ?? null;\n }\n return false;\n}\n\nexport function _cleanupCommittedDescendants<S>(\n parentId: string,\n transactions: Map<string, Transaction<S, any>>,\n): void {\n for (const [id, tx] of transactions) {\n if (tx.parentId === parentId && tx.status === \"committed\") {\n transactions.delete(id);\n _cleanupCommittedDescendants(id, transactions);\n }\n }\n}\n\n// ─── Transaction ────────────────────────────────────────────────────────────\n//\n// 每个 Transaction 对象是一个句柄,闭包绑定到其 id 和 generation。\n// 当 createTx 用相同 id 替换活跃事务时(去重机制),旧句柄变为\"过期\"——\n// 其 generation 不再匹配 generationRef,transactionsRef 中该 id 对应的\n// 对象也已替换。对过期句柄的所有操作会被静默忽略或抛出错误,防止过期\n// 的异步回调干扰新事务。\n//\n// parentId 是故意可变的(非 readonly)。当父事务回滚且 onError:\"commit\"\n// 的子事务被保留时,子事务的 parentId 被设为 null——它成为独立的根事务,\n// 因为它的父事务已不存在。\n// ────────────────────────────────────────────────────────────────────────────\n\nexport class Transaction<S, A> implements TransactionHandle<A> {\n readonly id: string;\n parentId: string | null;\n readonly onError: OnErrorStrategy;\n readonly generation: number;\n snapshot: S;\n readonly snapshotIndex: number;\n status: \"active\" | \"committed\" | \"rolledback\" = \"active\";\n cancelCallbacks: (() => void)[] = [];\n\n private engine: TransactionEngine<S, A>;\n\n constructor(\n engine: TransactionEngine<S, A>,\n id: string,\n parentId: string | null,\n onError: OnErrorStrategy,\n generation: number,\n snapshot: S,\n snapshotIndex: number,\n ) {\n this.engine = engine;\n this.id = id;\n this.parentId = parentId;\n this.onError = onError;\n this.generation = generation;\n this.snapshot = snapshot;\n this.snapshotIndex = snapshotIndex;\n }\n\n // 句柄过期的条件:\n // 1. transactionsRef 中该 id 对应的是不同的对象(被 createTx 替换),\n // 2. 或该句柄的 status 不再是 \"active\"(已提交/已回滚)。\n // 两个条件都必须检查——回滚后句柄从 transactionsRef 中删除,\n // 所以 `current !== this` 为 true;提交后 status 变化,\n // 所以 `this.status !== \"active\"` 为 true。\n isStale(): boolean {\n const current = this.engine.transactionsRef.current.get(this.id);\n return current !== this || this.status !== \"active\";\n }\n\n onCancel(callback: () => void): void {\n if (this.isStale()) {\n callback();\n return;\n }\n this.cancelCallbacks.push(callback);\n }\n\n dispatch(action: A): void {\n if (this.isStale()) return;\n this.engine.actionLogRef.current.push({\n action,\n txId: this.id,\n generation: this.generation,\n });\n this.engine._applyAction(action);\n }\n\n // spawn 在创建子事务前执行三重过期检查:\n // 1. 身份检查:transactionsRef 中该 id 必须持有此对象本身\n // 2. 状态检查:此句柄的 status 必须仍为 \"active\"\n // 3. generation 检查:此句柄的 generation 必须匹配 generationRef\n // generation 检查至关重要:当 createTx 用相同 id 替换事务时,\n // 旧句柄的 generation 不匹配 generationRef 中的新 generation。\n // 没有此检查,过期句柄可能在新事务下派生子事务。\n spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R {\n const current = this.engine.transactionsRef.current.get(this.id);\n if (\n current !== this ||\n this.status !== \"active\" ||\n this.generation !== this.engine.generationRef.current.get(this.id)\n ) {\n throw new Error(`Cannot spawn from transaction \"${this.id}\": parent is no longer active`);\n }\n // 子事务 id 就是用户传入的值——不与父事务 id 自动拼接。\n // 这意味着用户控制完整 id,并负责避免意外冲突。\n // 去重机制(createTx 回滚相同 id 的活跃事务)提供了\n // 有意的取消功能:例如 start({id:\"validate_name\"}) 会取消\n // 之前的 validate_name 任务。\n const childId = options?.id ?? _generateId();\n const childOnError = options?.onError ?? \"rollback\";\n const childOnDuplicate = options?.onDuplicate ?? this.engine.options?.onDuplicate ?? \"rollback\";\n if (childOnDuplicate === \"reuse\" && options?.id) {\n const existingChild = this.engine.transactionsRef.current.get(options.id);\n if (existingChild?.status === \"active\") {\n throw new Error(`Cannot spawn: transaction \"${options.id}\" is already active`);\n }\n }\n const childTx = this.engine._createTx(childId, this.id, childOnError, childOnDuplicate);\n return this.engine._runWithTx(childTx, task);\n }\n\n commit(): void {\n if (this.isStale()) return;\n this._commit();\n }\n\n rollback(): void {\n if (this.isStale()) return;\n this._rollback();\n }\n\n // ─── _commit ──────────────────────────────────────────────────────────\n //\n // 子事务提交:仅将 status 标记为 \"committed\"。记录保留在\n // transactionsRef 中,父事务仍可管理它(父事务回滚也会回滚\n // 已提交的子事务)。父事务的 _commit 或 _rollback 最终会\n // 清理已提交子事务的记录。\n //\n // 根事务提交:通过以下步骤完成事务:\n // 1. 将此事务(及其后代)的所有 action log 条目重新标记为\n // 普通 dispatch(txId: null)。这使它们成为永久性的——\n // 在任何未来的回滚重放中都会保留。\n // 2. 从 transactionsRef 中删除根事务记录。\n // 3. 清理已提交的后代记录(根事务已消失,这些记录不再有意义)。\n // 4. 如果没有活跃事务剩余,清空整个 action log 和事务映射——\n // 不可能再发生回滚,日志是不必要的开销。\n // ────────────────────────────────────────────────────────────────────────\n _commit(): void {\n if (this.parentId !== null) {\n this.status = \"committed\";\n } else {\n this.status = \"committed\";\n\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (entry.skipped) continue;\n if (\n entry.txId === this.id ||\n _isDescendantOf(entry.txId, this.id, this.engine.transactionsRef.current)\n ) {\n this.engine.actionLogRef.current[i] = {\n action: entry.action,\n txId: null,\n generation: 0,\n };\n }\n }\n\n this.engine.transactionsRef.current.delete(this.id);\n _cleanupCommittedDescendants(this.id, this.engine.transactionsRef.current);\n\n if (!this._hasActiveTransactions()) {\n this.engine.actionLogRef.current = [];\n this.engine.transactionsRef.current.clear();\n this.engine.generationRef.current.clear();\n }\n }\n }\n\n // ─── _rollback ────────────────────────────────────────────────────────\n //\n // 回滚不是简单的\"恢复快照\"——而是快照 + 重放。\n // 这确保回滚仅撤销目标事务的变更,同时保留以下变更:\n // - 事务期间发生的普通(非事务)dispatch\n // - 并发兄弟事务的 action\n // - onError:\"commit\" 的后代(提交边界)\n //\n // 算法步骤:\n // 阶段 1:将后代分类为 preserveSet 和 rollbackSet\n // 阶段 2:在 action log 中将 rollbackSet 的 action 标记为 skipped\n // 阶段 3:将 preserveSet 的 action 重新标记为普通 dispatch(txId: null)\n // 阶段 4:从快照重放,跳过已回滚的 action\n // 阶段 5:更新状态、删除记录、分离保留的事务\n // 阶段 6:若无活跃事务剩余则最终清理\n //\n // ── 阶段 1:preserveSet 分类 ──────────────────────────────────────\n //\n // onError:\"commit\" 创建\"提交边界\"——其下的整个子树被保留。\n // 不能选择性回滚提交子树内的后代,因为它们的 dispatch 是基于\n // 父事务中间状态计算的;移除父事务的 action 会使后代的 dispatch\n // 不一致。\n //\n // `underPreserve` 遍历防止双重保留:如果 onError:\"commit\" 的后代\n // 有一个祖先已在 preserveSet 中,它已被该祖先的提交边界覆盖,\n // 不需要单独保留。这在菱形事务层级中很重要,同一后代可能通过\n // 多条路径到达。\n //\n // ── 阶段 3:重新标记 preserveSet ──────────────────────────────────\n //\n // 仅将已提交的保留子事务的 action 重新标记为普通 dispatch(txId: null)。\n // 活跃的保留子事务保持原 txId,以便后续回滚/提交时能识别自己的 action。\n // 已提交的保留子事务即将被删除,其 action 需变为永久性的。\n //\n // 必须在阶段 2 之后执行,以免保留的 action(txId 变为 null 后)\n // 被 rollbackSet 检查意外捕获。\n //\n // ── 阶段 5:分离保留的事务 ────────────────────────────────────────\n //\n // 回滚后,保留的事务需要变为独立的:\n // - 已提交的保留事务:删除(其工作已完成,action 已成为日志中的\n // 普通 dispatch)\n // - 活跃的保留事务:parentId 设为 null(成为根事务,\n // 因为已回滚的父事务不再存在),并更新 snapshot 为不含自身\n // action 的正确基础状态(从回滚事务的 snapshot 重放,\n // 跳过 rollbackSet 和子事务自己的 action)\n //\n // 然后对每个活跃保留事务调用 _cleanupCommittedDescendants,\n // 清理其子树中已提交的子事务。现在安全了,因为保留事务已是独立根。\n // ────────────────────────────────────────────────────────────────────────\n _rollback(): void {\n if (this.isStale()) return;\n\n const descendants = _getAllDescendants(this.id, this.engine.transactionsRef.current);\n\n // 阶段 1:分类后代\n const preserveSet = new Set<string>();\n const visited = new Set<string>();\n for (const descId of descendants) {\n if (visited.has(descId)) continue;\n const descTx = this.engine.transactionsRef.current.get(descId);\n if (descTx?.onError === \"commit\") {\n // 从 descTx 向上遍历到 this.id,检查是否有中间祖先\n // 已在 preserveSet 中。如果有,descTx 已被该祖先的\n // 提交边界覆盖。\n let underPreserve = false;\n let current: string | null = descTx.parentId;\n while (current !== null && current !== this.id) {\n if (preserveSet.has(current)) {\n underPreserve = true;\n break;\n }\n current = this.engine.transactionsRef.current.get(current)?.parentId ?? null;\n }\n if (!underPreserve) {\n // 此后代是提交边界。保留它及其整个子树\n // (不能部分保留子树)。\n preserveSet.add(descId);\n const subDescendants = _getAllDescendants(descId, this.engine.transactionsRef.current);\n for (const subId of subDescendants) {\n preserveSet.add(subId);\n visited.add(subId);\n }\n }\n }\n visited.add(descId);\n }\n\n const rollbackSet = new Set<string>();\n rollbackSet.add(this.id);\n for (const descId of descendants) {\n if (!preserveSet.has(descId)) {\n rollbackSet.add(descId);\n }\n }\n\n // 阶段 2:将回滚 action 标记为 skipped\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (entry.txId !== null && rollbackSet.has(entry.txId)) {\n entry.skipped = true;\n }\n }\n\n // 阶段 3:将保留的 action 重新标记为普通 dispatch\n // 必须在阶段 2 之后执行,以免保留的 action(txId 变为 null 后)\n // 被 rollbackSet 检查意外捕获。\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (!entry.skipped && entry.txId !== null && preserveSet.has(entry.txId)) {\n const preservedTx = this.engine.transactionsRef.current.get(entry.txId);\n if (preservedTx?.status === \"committed\") {\n this.engine.actionLogRef.current[i] = {\n action: entry.action,\n txId: null,\n generation: 0,\n };\n }\n }\n }\n\n // 阶段 4:从快照重放,跳过已回滚的 action\n let replayState = this.snapshot;\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (entry.skipped) continue;\n replayState = this.engine.reducer(replayState, entry.action);\n }\n\n // 在删除前将回滚记录标记为 rolledback(可能被尚未完成的\n // 异步回调引用)\n for (const id of rollbackSet) {\n const record = this.engine.transactionsRef.current.get(id);\n if (record) record.status = \"rolledback\";\n }\n\n this.engine.stateRef.current = replayState;\n this.engine._notify();\n\n // 触发 rollbackSet 中每个事务的 onCancel 回调。\n // 使用 copy-and-clear 模式防止双重触发和重入注册。\n for (const id of rollbackSet) {\n const record = this.engine.transactionsRef.current.get(id);\n if (record?.cancelCallbacks.length) {\n const callbacks = [...record.cancelCallbacks];\n record.cancelCallbacks = [];\n for (const cb of callbacks) cb();\n }\n }\n\n // 从 transactionsRef 中删除已回滚的记录\n for (const id of rollbackSet) {\n this.engine.transactionsRef.current.delete(id);\n }\n\n // 阶段 5:分离保留的事务\n for (const id of preserveSet) {\n const record = this.engine.transactionsRef.current.get(id);\n if (record) {\n if (record.status === \"committed\") {\n this.engine.transactionsRef.current.delete(id);\n } else if (record.status === \"active\") {\n const needsDetach = record.parentId !== null && rollbackSet.has(record.parentId);\n if (needsDetach) {\n record.parentId = null;\n const childDescendants = _getAllDescendants(id, this.engine.transactionsRef.current);\n const childOwnSet = new Set<string>();\n childOwnSet.add(id);\n for (const descId of childDescendants) {\n childOwnSet.add(descId);\n }\n let newSnapshot = this.snapshot;\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (entry.skipped) continue;\n if (\n entry.txId !== null &&\n (rollbackSet.has(entry.txId) || childOwnSet.has(entry.txId))\n )\n continue;\n newSnapshot = this.engine.reducer(newSnapshot, entry.action);\n }\n record.snapshot = newSnapshot;\n }\n }\n }\n }\n\n // 清理每个活跃保留子树中已提交的后代。\n // 现在安全了,因为保留事务已是独立根。\n for (const id of preserveSet) {\n const record = this.engine.transactionsRef.current.get(id);\n if (record?.status === \"active\") {\n _cleanupCommittedDescendants(id, this.engine.transactionsRef.current);\n }\n }\n\n // 阶段 6:若无活跃事务剩余,清空所有内容。\n // action log 仅用于回滚重放;没有活跃事务就不可能回滚,\n // 日志纯属开销。\n if (!this._hasActiveTransactions()) {\n this.engine.actionLogRef.current = [];\n this.engine.transactionsRef.current.clear();\n this.engine.generationRef.current.clear();\n }\n }\n\n // 仅回滚直接的活跃子事务,而非所有后代。\n // 每个子事务的 _rollback() 递归处理自己的子树,\n // 包括自己的 onError:\"commit\" 边界。\n _rollbackActiveDescendants(): void {\n const activeChildren: string[] = [];\n for (const [, tx] of this.engine.transactionsRef.current) {\n if (tx.parentId === this.id && tx.status === \"active\") {\n activeChildren.push(tx.id);\n }\n }\n for (const id of activeChildren) {\n const tx = this.engine.transactionsRef.current.get(id);\n if (tx?.status === \"active\") {\n tx._rollback();\n }\n }\n let replayState = this.snapshot;\n for (let i = this.snapshotIndex; i < this.engine.actionLogRef.current.length; i++) {\n const entry = this.engine.actionLogRef.current[i]!;\n if (entry.skipped) continue;\n replayState = this.engine.reducer(replayState, entry.action);\n }\n this.engine.stateRef.current = replayState;\n this.engine._notify();\n }\n\n private _hasActiveTransactions(): boolean {\n for (const tx of this.engine.transactionsRef.current.values()) {\n if (tx.status === \"active\") return true;\n }\n return false;\n }\n}\n","import {\n Transaction,\n _generateId,\n _isDescendantOf,\n _cleanupCommittedDescendants,\n type Ref,\n type ActionLogEntry,\n type OnErrorStrategy,\n type OnDuplicateStrategy,\n type TransactionOptions,\n type TransactionHandle,\n type TransactionalReducerOptions,\n type TransactionEngine,\n} from \"./Transaction\";\n\nexport type {\n Ref,\n ActionLogEntry,\n OnErrorStrategy,\n OnDuplicateStrategy,\n TransactionOptions,\n TransactionHandle,\n TransactionalReducerOptions,\n};\n\nexport type { Transaction };\n\nexport class TransactionalReducer<S, A> implements TransactionEngine<S, A> {\n readonly reducer: (state: S, action: A) => S;\n readonly options: TransactionalReducerOptions<S> | undefined;\n readonly stateRef: Ref<S>;\n readonly actionLogRef: Ref<ActionLogEntry<A>[]>;\n readonly transactionsRef: Ref<Map<string, Transaction<S, A>>>;\n readonly generationRef: Ref<Map<string, number>>;\n\n private _listeners = new Set<(state: S) => void>();\n\n constructor(\n reducer: (state: S, action: A) => S,\n initialState: S,\n options?: TransactionalReducerOptions<S>,\n ) {\n this.reducer = reducer;\n this.options = options;\n this.stateRef = { current: initialState };\n this.actionLogRef = { current: [] };\n this.transactionsRef = { current: new Map() };\n this.generationRef = { current: new Map() };\n }\n\n get state(): S {\n return this.stateRef.current;\n }\n\n subscribe(listener: (state: S) => void): () => void {\n this._listeners.add(listener);\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n _notify(): void {\n const state = this.stateRef.current;\n for (const listener of this._listeners) {\n listener(state);\n }\n }\n\n // 非事务 dispatch 仅在有活跃事务时记录日志。\n // 这确保它们在回滚重放中被保留(txId:null,不在任何 rollbackSet 中)。\n // 无活跃事务时日志不必要——不可能发生回滚,因此跳过日志记录。\n dispatch(action: A): void {\n if (this._hasActiveTransactions()) {\n this.actionLogRef.current.push({ action, txId: null, generation: 0 });\n }\n this._applyAction(action);\n }\n\n run<R>(task: (tx: TransactionHandle<A>) => R, options?: TransactionOptions): R {\n const strategy = options?.onDuplicate ?? this.options?.onDuplicate ?? \"rollback\";\n if (strategy === \"reuse\" && options?.id) {\n const existing = this.transactionsRef.current.get(options.id);\n if (existing?.status === \"active\") {\n throw new Error(`Cannot run: transaction \"${options.id}\" is already active`);\n }\n }\n const tx = this._createTx(options?.id, null, options?.onError ?? \"rollback\", strategy);\n return this._runWithTx(tx, task);\n }\n\n create(options?: TransactionOptions): TransactionHandle<A> {\n const strategy = options?.onDuplicate ?? this.options?.onDuplicate ?? \"rollback\";\n if (strategy === \"reuse\" && options?.id) {\n const existing = this.transactionsRef.current.get(options.id);\n if (existing?.status === \"active\") return existing;\n }\n return this._createTx(options?.id, null, options?.onError ?? \"rollback\", strategy);\n }\n\n getTransaction(id: string): TransactionHandle<A> | undefined {\n return this.transactionsRef.current.get(id);\n }\n\n _applyAction(action: A): void {\n this.stateRef.current = this.reducer(this.stateRef.current, action);\n this._notify();\n }\n\n // ─── _createTx ────────────────────────────────────────────────────────\n //\n // 去重机制:如果相同 id 的活跃事务已存在,根据 onDuplicate 策略处理:\n // - rollback:回滚旧事务再创建新的(默认,向后兼容)\n // - commit:提交旧事务(含回滚其活跃子事务)再创建新的\n // - reject:抛出错误,拒绝创建\n // - reuse:不在 _createTx 内处理——由调用点(api.create/run/spawn)处理\n //\n // 回滚旧事务后,创建新的 Transaction 对象并赋予新的 generation。\n // 旧句柄变为过期,因为:\n // - transactionsRef 中该 id 现在持有新对象\n // - generationRef 中该 id 现有更高的 generation\n //\n // 关键:旧句柄的异步完成回调绝不能对过期句柄调用\n // _commit 或 _rollback,因为:\n // - _commit 会从 transactionsRef 删除新事务(相同 id 键),\n // 破坏新事务的状态\n // - _rollbackActiveDescendants 会找到新事务的子事务\n // (parentId === this.id 匹配)并错误地回滚它们\n // 这就是 runWithTx 在 _commit/_rollback 前检查过期的原因。\n // ────────────────────────────────────────────────────────────────────────\n _createTx(\n id: string | undefined,\n parentId: string | null,\n onError: OnErrorStrategy,\n onDuplicate: OnDuplicateStrategy,\n ): Transaction<S, A> {\n const txId = id || this.options?.idGenerator?.() || _generateId();\n const existing = this.transactionsRef.current.get(txId);\n if (existing?.status === \"active\") {\n switch (onDuplicate) {\n case \"rollback\":\n existing._rollback();\n break;\n case \"commit\":\n existing._rollbackActiveDescendants();\n existing._commit();\n if (existing.parentId !== null) {\n for (let i = existing.snapshotIndex; i < this.actionLogRef.current.length; i++) {\n const entry = this.actionLogRef.current[i]!;\n if (entry.skipped) continue;\n if (\n entry.txId === existing.id ||\n _isDescendantOf(entry.txId, existing.id, this.transactionsRef.current)\n ) {\n this.actionLogRef.current[i] = {\n action: entry.action,\n txId: null,\n generation: 0,\n };\n }\n }\n _cleanupCommittedDescendants(existing.id, this.transactionsRef.current);\n }\n break;\n case \"reject\":\n throw new Error(`Transaction \"${txId}\" is already active`);\n case \"reuse\":\n break;\n }\n }\n\n const generation = this._nextGeneration(txId);\n const snapshot = (this.options?.snapshot ?? structuredClone)(this.stateRef.current);\n const snapshotIndex = this.actionLogRef.current.length;\n\n const tx = new Transaction(this, txId, parentId, onError, generation, snapshot, snapshotIndex);\n\n this.transactionsRef.current.set(txId, tx);\n return tx;\n }\n\n // ─── _runWithTx ───────────────────────────────────────────────────────\n //\n // 包装任务函数,提供自动事务生命周期管理:\n // - 成功 → 回滚活跃后代,然后提交\n // - onError:\"rollback\" 的错误 → 回滚事务\n // - onError:\"commit\" 的错误 → 回滚活跃后代,然后提交\n //\n // 对于异步任务,Promise 回调中的过期检查至关重要。\n // 在任务启动和 Promise resolve 之间,事务可能已过期\n // (例如 _createTx 用相同 id 替换了它)。过期句柄绝不能\n // 提交或回滚,因为:\n // - 对过期根事务的 _commit 会从 transactionsRef 删除新事务\n // (共享相同 id 键)\n // - 对过期句柄的 _rollbackActiveDescendants 会找到新事务的\n // 子事务(parentId === this.id 匹配)并错误地回滚它们\n //\n // 对于同步任务,执行期间不可能过期(无异步暂停),无需检查。\n // ────────────────────────────────────────────────────────────────────────\n _runWithTx<R>(tx: Transaction<S, A>, task: (tx: TransactionHandle<A>) => R): R {\n try {\n const result = task(tx);\n if (result instanceof Promise) {\n return result.then(\n (r) => {\n // 过期检查:如果事务已被替换(例如第二次 run 使用相同 id),\n // 跳过提交——新事务现在拥有该 id。\n if (!tx.isStale()) {\n tx._rollbackActiveDescendants();\n tx._commit();\n }\n return r;\n },\n (e) => {\n if (tx.onError === \"commit\") {\n // onError:\"commit\" 表示出错时保留变更。\n // 仍需过期检查——过期句柄绝不能提交\n // (会从 transactionsRef 删除新事务)。\n if (!tx.isStale()) {\n tx._rollbackActiveDescendants();\n tx._commit();\n }\n } else {\n // _rollback 内部有自己的过期检查,\n // 此处无需额外检查。\n tx._rollback();\n }\n throw e;\n },\n ) as unknown as R;\n }\n // 同步成功:同步执行期间不可能过期\n tx._rollbackActiveDescendants();\n tx._commit();\n return result;\n } catch (e) {\n // 同步错误:同样不可能过期\n if (tx.onError === \"commit\") {\n tx._rollbackActiveDescendants();\n tx._commit();\n } else {\n tx._rollback();\n }\n throw e;\n }\n }\n\n private _nextGeneration(txId: string): number {\n const prev = this.generationRef.current.get(txId) ?? 0;\n const next = prev + 1;\n this.generationRef.current.set(txId, next);\n return next;\n }\n\n private _hasActiveTransactions(): boolean {\n for (const tx of this.transactionsRef.current.values()) {\n if (tx.status === \"active\") return true;\n }\n return false;\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transactional-reducer/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A transactional state management library with reducer pattern",
|
|
5
|
-
"main": "dist/
|
|
6
|
-
"types": "dist/
|
|
5
|
+
"main": "dist/index.mjs",
|
|
6
|
+
"types": "dist/index.d.mts",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"import": {
|
|
11
|
-
"types": "./dist/TransactionalReducer.d.ts",
|
|
12
|
-
"default": "./dist/TransactionalReducer.js"
|
|
13
|
-
},
|
|
14
|
-
"require": {
|
|
15
|
-
"types": "./dist/TransactionalReducer.d.ts",
|
|
16
|
-
"default": "./dist/TransactionalReducer.js"
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
8
|
"files": [
|
|
21
9
|
"dist"
|
|
22
10
|
],
|
|
@@ -37,11 +25,12 @@
|
|
|
37
25
|
"directory": "packages/core"
|
|
38
26
|
},
|
|
39
27
|
"devDependencies": {
|
|
28
|
+
"tsdown": "^0.22.0",
|
|
40
29
|
"typescript": "^6.0.3",
|
|
41
30
|
"vitest": "^4.1.7"
|
|
42
31
|
},
|
|
43
32
|
"scripts": {
|
|
44
33
|
"test": "vitest --run",
|
|
45
|
-
"build": "
|
|
34
|
+
"build": "tsdown"
|
|
46
35
|
}
|
|
47
36
|
}
|
package/dist/Transaction.d.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
export interface Ref<T> {
|
|
2
|
-
current: T;
|
|
3
|
-
}
|
|
4
|
-
export interface ActionLogEntry<A> {
|
|
5
|
-
action: A;
|
|
6
|
-
txId: string | null;
|
|
7
|
-
generation: number;
|
|
8
|
-
skipped?: boolean;
|
|
9
|
-
}
|
|
10
|
-
export type OnErrorStrategy = "rollback" | "commit";
|
|
11
|
-
export type OnDuplicateStrategy = "rollback" | "reuse" | "commit" | "reject";
|
|
12
|
-
export interface TransactionOptions {
|
|
13
|
-
id?: string;
|
|
14
|
-
onError?: OnErrorStrategy;
|
|
15
|
-
onDuplicate?: OnDuplicateStrategy;
|
|
16
|
-
}
|
|
17
|
-
export type SpawnOptions = TransactionOptions;
|
|
18
|
-
export interface TransactionHandle<A> {
|
|
19
|
-
readonly id: string;
|
|
20
|
-
readonly parentId: string | null;
|
|
21
|
-
readonly onError: OnErrorStrategy;
|
|
22
|
-
dispatch(action: A): void;
|
|
23
|
-
spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R;
|
|
24
|
-
commit(): void;
|
|
25
|
-
rollback(): void;
|
|
26
|
-
isStale(): boolean;
|
|
27
|
-
onCancel(callback: () => void): void;
|
|
28
|
-
}
|
|
29
|
-
export interface TransactionalReducerOptions<S = any> {
|
|
30
|
-
idGenerator?: () => string;
|
|
31
|
-
snapshot?: (state: S) => S;
|
|
32
|
-
onDuplicate?: OnDuplicateStrategy;
|
|
33
|
-
}
|
|
34
|
-
export interface TransactionEngine<S, A> {
|
|
35
|
-
readonly reducer: (state: S, action: A) => S;
|
|
36
|
-
readonly options: TransactionalReducerOptions<S> | undefined;
|
|
37
|
-
readonly stateRef: Ref<S>;
|
|
38
|
-
readonly actionLogRef: Ref<ActionLogEntry<A>[]>;
|
|
39
|
-
readonly transactionsRef: Ref<Map<string, Transaction<S, A>>>;
|
|
40
|
-
readonly generationRef: Ref<Map<string, number>>;
|
|
41
|
-
_createTx(id: string | undefined, parentId: string | null, onError: OnErrorStrategy, onDuplicate: OnDuplicateStrategy): Transaction<S, A>;
|
|
42
|
-
_runWithTx<R>(tx: Transaction<S, A>, task: (tx: TransactionHandle<A>) => R): R;
|
|
43
|
-
_applyAction(action: A): void;
|
|
44
|
-
_notify(): void;
|
|
45
|
-
}
|
|
46
|
-
export declare function _generateId(): string;
|
|
47
|
-
export declare function _getAllDescendants<S>(txId: string, transactions: Map<string, Transaction<S, any>>): string[];
|
|
48
|
-
export declare function _isDescendantOf<S>(candidateTxId: string | null, ancestorTxId: string, transactions: Map<string, Transaction<S, any>>): boolean;
|
|
49
|
-
export declare function _cleanupCommittedDescendants<S>(parentId: string, transactions: Map<string, Transaction<S, any>>): void;
|
|
50
|
-
export declare class Transaction<S, A> implements TransactionHandle<A> {
|
|
51
|
-
readonly id: string;
|
|
52
|
-
parentId: string | null;
|
|
53
|
-
readonly onError: OnErrorStrategy;
|
|
54
|
-
readonly generation: number;
|
|
55
|
-
snapshot: S;
|
|
56
|
-
readonly snapshotIndex: number;
|
|
57
|
-
status: "active" | "committed" | "rolledback";
|
|
58
|
-
cancelCallbacks: (() => void)[];
|
|
59
|
-
private engine;
|
|
60
|
-
constructor(engine: TransactionEngine<S, A>, id: string, parentId: string | null, onError: OnErrorStrategy, generation: number, snapshot: S, snapshotIndex: number);
|
|
61
|
-
isStale(): boolean;
|
|
62
|
-
onCancel(callback: () => void): void;
|
|
63
|
-
dispatch(action: A): void;
|
|
64
|
-
spawn<R>(task: (tx: TransactionHandle<A>) => R, options?: SpawnOptions): R;
|
|
65
|
-
commit(): void;
|
|
66
|
-
rollback(): void;
|
|
67
|
-
_commit(): void;
|
|
68
|
-
_rollback(): void;
|
|
69
|
-
_rollbackActiveDescendants(): void;
|
|
70
|
-
private _hasActiveTransactions;
|
|
71
|
-
}
|
|
72
|
-
//# sourceMappingURL=Transaction.d.ts.map
|