@matter/general 0.12.4-alpha.0-20250211-56b2c53a0 → 0.12.4-alpha.0-20250213-1187f81eb
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/cjs/MatterError.d.ts +12 -0
- package/dist/cjs/MatterError.d.ts.map +1 -1
- package/dist/cjs/MatterError.js +12 -0
- package/dist/cjs/MatterError.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/log/Logger.d.ts.map +1 -1
- package/dist/cjs/log/Logger.js +2 -0
- package/dist/cjs/log/Logger.js.map +1 -1
- package/dist/cjs/time/Time.d.ts +1 -1
- package/dist/cjs/time/Time.d.ts.map +1 -1
- package/dist/cjs/time/Time.js +2 -2
- package/dist/cjs/time/Time.js.map +1 -1
- package/dist/cjs/transaction/Participant.d.ts +47 -0
- package/dist/cjs/transaction/Participant.d.ts.map +1 -0
- package/dist/cjs/transaction/Participant.js +22 -0
- package/dist/cjs/transaction/Participant.js.map +6 -0
- package/dist/cjs/transaction/Resource.d.ts +29 -0
- package/dist/cjs/transaction/Resource.d.ts.map +1 -0
- package/dist/cjs/transaction/Resource.js +40 -0
- package/dist/cjs/transaction/Resource.js.map +6 -0
- package/dist/cjs/transaction/ResourceSet.d.ts +36 -0
- package/dist/cjs/transaction/ResourceSet.d.ts.map +1 -0
- package/dist/cjs/transaction/ResourceSet.js +155 -0
- package/dist/cjs/transaction/ResourceSet.js.map +6 -0
- package/dist/cjs/transaction/Status.d.ts +49 -0
- package/dist/cjs/transaction/Status.d.ts.map +1 -0
- package/dist/cjs/transaction/Status.js +55 -0
- package/dist/cjs/transaction/Status.js.map +6 -0
- package/dist/cjs/transaction/Transaction.d.ts +197 -0
- package/dist/cjs/transaction/Transaction.d.ts.map +1 -0
- package/dist/cjs/transaction/Transaction.js +50 -0
- package/dist/cjs/transaction/Transaction.js.map +6 -0
- package/dist/cjs/transaction/Tx.d.ts +47 -0
- package/dist/cjs/transaction/Tx.d.ts.map +1 -0
- package/dist/cjs/transaction/Tx.js +586 -0
- package/dist/cjs/transaction/Tx.js.map +6 -0
- package/dist/cjs/transaction/errors.d.ts +52 -0
- package/dist/cjs/transaction/errors.d.ts.map +1 -0
- package/dist/cjs/transaction/errors.js +47 -0
- package/dist/cjs/transaction/errors.js.map +6 -0
- package/dist/cjs/transaction/index.d.ts +8 -0
- package/dist/cjs/transaction/index.d.ts.map +1 -0
- package/dist/cjs/transaction/index.js +25 -0
- package/dist/cjs/transaction/index.js.map +6 -0
- package/dist/cjs/util/Cancelable.d.ts +101 -0
- package/dist/cjs/util/Cancelable.d.ts.map +1 -0
- package/dist/cjs/util/Cancelable.js +279 -0
- package/dist/cjs/util/Cancelable.js.map +6 -0
- package/dist/cjs/util/Observable.js +1 -1
- package/dist/cjs/util/Observable.js.map +1 -1
- package/dist/cjs/util/Promises.d.ts +0 -15
- package/dist/cjs/util/Promises.d.ts.map +1 -1
- package/dist/cjs/util/Promises.js +0 -33
- package/dist/cjs/util/Promises.js.map +1 -1
- package/dist/cjs/util/index.d.ts +1 -0
- package/dist/cjs/util/index.d.ts.map +1 -1
- package/dist/cjs/util/index.js +1 -0
- package/dist/cjs/util/index.js.map +1 -1
- package/dist/esm/MatterError.d.ts +12 -0
- package/dist/esm/MatterError.d.ts.map +1 -1
- package/dist/esm/MatterError.js +12 -0
- package/dist/esm/MatterError.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/log/Logger.d.ts.map +1 -1
- package/dist/esm/log/Logger.js +2 -0
- package/dist/esm/log/Logger.js.map +1 -1
- package/dist/esm/time/Time.d.ts +1 -1
- package/dist/esm/time/Time.d.ts.map +1 -1
- package/dist/esm/time/Time.js +1 -1
- package/dist/esm/transaction/Participant.d.ts +47 -0
- package/dist/esm/transaction/Participant.d.ts.map +1 -0
- package/dist/esm/transaction/Participant.js +6 -0
- package/dist/esm/transaction/Participant.js.map +6 -0
- package/dist/esm/transaction/Resource.d.ts +29 -0
- package/dist/esm/transaction/Resource.d.ts.map +1 -0
- package/dist/esm/transaction/Resource.js +20 -0
- package/dist/esm/transaction/Resource.js.map +6 -0
- package/dist/esm/transaction/ResourceSet.d.ts +36 -0
- package/dist/esm/transaction/ResourceSet.d.ts.map +1 -0
- package/dist/esm/transaction/ResourceSet.js +135 -0
- package/dist/esm/transaction/ResourceSet.js.map +6 -0
- package/dist/esm/transaction/Status.d.ts +49 -0
- package/dist/esm/transaction/Status.d.ts.map +1 -0
- package/dist/esm/transaction/Status.js +35 -0
- package/dist/esm/transaction/Status.js.map +6 -0
- package/dist/esm/transaction/Transaction.d.ts +197 -0
- package/dist/esm/transaction/Transaction.d.ts.map +1 -0
- package/dist/esm/transaction/Transaction.js +30 -0
- package/dist/esm/transaction/Transaction.js.map +6 -0
- package/dist/esm/transaction/Tx.d.ts +47 -0
- package/dist/esm/transaction/Tx.d.ts.map +1 -0
- package/dist/esm/transaction/Tx.js +566 -0
- package/dist/esm/transaction/Tx.js.map +6 -0
- package/dist/esm/transaction/errors.d.ts +52 -0
- package/dist/esm/transaction/errors.d.ts.map +1 -0
- package/dist/esm/transaction/errors.js +27 -0
- package/dist/esm/transaction/errors.js.map +6 -0
- package/dist/esm/transaction/index.d.ts +8 -0
- package/dist/esm/transaction/index.d.ts.map +1 -0
- package/dist/esm/transaction/index.js +8 -0
- package/dist/esm/transaction/index.js.map +6 -0
- package/dist/esm/util/Cancelable.d.ts +101 -0
- package/dist/esm/util/Cancelable.d.ts.map +1 -0
- package/dist/esm/util/Cancelable.js +259 -0
- package/dist/esm/util/Cancelable.js.map +6 -0
- package/dist/esm/util/Observable.js +1 -1
- package/dist/esm/util/Observable.js.map +1 -1
- package/dist/esm/util/Promises.d.ts +0 -15
- package/dist/esm/util/Promises.d.ts.map +1 -1
- package/dist/esm/util/Promises.js +0 -33
- package/dist/esm/util/Promises.js.map +1 -1
- package/dist/esm/util/index.d.ts +1 -0
- package/dist/esm/util/index.d.ts.map +1 -1
- package/dist/esm/util/index.js +1 -0
- package/dist/esm/util/index.js.map +1 -1
- package/package.json +2 -2
- package/src/MatterError.ts +18 -0
- package/src/index.ts +1 -0
- package/src/log/Logger.ts +3 -0
- package/src/time/Time.ts +1 -1
- package/src/transaction/Participant.ts +54 -0
- package/src/transaction/Resource.ts +39 -0
- package/src/transaction/ResourceSet.ts +160 -0
- package/src/transaction/Status.ts +68 -0
- package/src/transaction/Transaction.ts +190 -0
- package/src/transaction/Tx.ts +734 -0
- package/src/transaction/errors.ts +53 -0
- package/src/transaction/index.ts +8 -0
- package/src/util/Cancelable.ts +380 -0
- package/src/util/Observable.ts +1 -1
- package/src/util/Promises.ts +0 -52
- package/src/util/index.ts +1 -0
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2022-2025 Matter.js Authors
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Diagnostic } from "#log/Diagnostic.js";
|
|
8
|
+
import { Logger } from "#log/Logger.js";
|
|
9
|
+
import { ImplementationError, ReadOnlyError } from "#MatterError.js";
|
|
10
|
+
import { Observable } from "#util/Observable.js";
|
|
11
|
+
import { MaybePromise } from "#util/Promises.js";
|
|
12
|
+
import { describeList } from "#util/String.js";
|
|
13
|
+
import { FinalizationError, TransactionDestroyedError, TransactionFlowError, UnsettledStateError } from "./errors.js";
|
|
14
|
+
import type { Participant } from "./Participant.js";
|
|
15
|
+
import type { Resource } from "./Resource.js";
|
|
16
|
+
import { ResourceSet } from "./ResourceSet.js";
|
|
17
|
+
import { Status } from "./Status.js";
|
|
18
|
+
import { Transaction } from "./Transaction.js";
|
|
19
|
+
|
|
20
|
+
const logger = Logger.get("Transaction");
|
|
21
|
+
|
|
22
|
+
// Controls the number of times we will cycle pre-commit handlers waiting for state to settle
|
|
23
|
+
const MAX_PRECOMMIT_CYCLES = 5;
|
|
24
|
+
|
|
25
|
+
// Controls the number of commits that may occur due to mutation in post-commit handlers
|
|
26
|
+
const MAX_CHAINED_COMMITS = 5;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This is the only public interface to this file.
|
|
30
|
+
*/
|
|
31
|
+
export function act<T>(via: string, actor: (transaction: Transaction) => T): T {
|
|
32
|
+
const tx = new Tx(via);
|
|
33
|
+
let commits = 0;
|
|
34
|
+
|
|
35
|
+
// Post-commit logic may result in the transaction requiring commit again so commit iteratively up to
|
|
36
|
+
// MAX_CHAINED_COMMITS times
|
|
37
|
+
function commitTransaction(finalResult: T): MaybePromise<T> {
|
|
38
|
+
commits++;
|
|
39
|
+
|
|
40
|
+
if (commits > MAX_CHAINED_COMMITS) {
|
|
41
|
+
throw new TransactionFlowError(
|
|
42
|
+
`Transaction commits have cascaded ${MAX_CHAINED_COMMITS} times which likely indicates an infinite loop`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Avoid MaybePromise.then to shorten stack traces
|
|
47
|
+
const result = tx.commit();
|
|
48
|
+
if (MaybePromise.is(result)) {
|
|
49
|
+
return result.then(() => {
|
|
50
|
+
if (tx.status === Transaction.Status.Exclusive) {
|
|
51
|
+
return commitTransaction(finalResult);
|
|
52
|
+
}
|
|
53
|
+
return finalResult;
|
|
54
|
+
});
|
|
55
|
+
} else if (tx.status === Transaction.Status.Exclusive) {
|
|
56
|
+
return commitTransaction(finalResult);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return finalResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleTransactionError = ((error: any) => {
|
|
63
|
+
// If we've committed, error happened during commit and we've already logged and cleaned up
|
|
64
|
+
if (commits) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.error("Rolling back", tx.via, "due to error:", Diagnostic.weak(error?.message || `${error}`));
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = tx.rollback();
|
|
72
|
+
if (MaybePromise.is(result)) {
|
|
73
|
+
return Promise.resolve(result).catch(error2 => {
|
|
74
|
+
if (error2 !== error) {
|
|
75
|
+
logger.error("Secondary error in", tx.via, "rollback:", error2);
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch (error2) {
|
|
81
|
+
if (error2 !== error) {
|
|
82
|
+
logger.error("Secondary error in", tx.via, "rollback:", error2);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
throw error;
|
|
87
|
+
}) as (error: any) => MaybePromise<T>; // Cast because otherwise type is MaybePromise<void>
|
|
88
|
+
|
|
89
|
+
const closeTransaction = tx.close.bind(tx);
|
|
90
|
+
|
|
91
|
+
let isAsync = false;
|
|
92
|
+
try {
|
|
93
|
+
// Execute the actor
|
|
94
|
+
const actorResult = actor(tx);
|
|
95
|
+
|
|
96
|
+
// If actor is async, chain commit and close asynchronously
|
|
97
|
+
if (MaybePromise.is(actorResult)) {
|
|
98
|
+
// If the actor is async mark the transaction as async; this will enable reporting on lock changes
|
|
99
|
+
isAsync = tx.isAsync = true;
|
|
100
|
+
return Promise.resolve(actorResult)
|
|
101
|
+
.then(commitTransaction, handleTransactionError)
|
|
102
|
+
.finally(closeTransaction) as T;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Actor is not async but if commit is, chain closeTransaction
|
|
106
|
+
const commitResult = commitTransaction(actorResult);
|
|
107
|
+
if (MaybePromise.is(commitResult)) {
|
|
108
|
+
isAsync = true;
|
|
109
|
+
return Promise.resolve(commitResult).catch(handleTransactionError).finally(closeTransaction) as T;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Fully synchronous action
|
|
113
|
+
return commitResult;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
const result = handleTransactionError(e);
|
|
116
|
+
|
|
117
|
+
// Above throws if synchronous so this is async code path
|
|
118
|
+
isAsync = true;
|
|
119
|
+
return Promise.resolve(result).finally(closeTransaction) as T;
|
|
120
|
+
} finally {
|
|
121
|
+
if (!isAsync) {
|
|
122
|
+
tx.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The concrete implementation of the Transaction interface.
|
|
129
|
+
*/
|
|
130
|
+
class Tx implements Transaction {
|
|
131
|
+
#participants = new Set<Participant>();
|
|
132
|
+
#roles = new Map<{}, Participant>();
|
|
133
|
+
#resources = new Set<Resource>();
|
|
134
|
+
#status;
|
|
135
|
+
#waitingOn?: Iterable<Transaction>;
|
|
136
|
+
#via: string;
|
|
137
|
+
#shared?: Observable<[]>;
|
|
138
|
+
#closed?: Observable<[]>;
|
|
139
|
+
#isAsync = false;
|
|
140
|
+
|
|
141
|
+
constructor(via: string, readonly = false) {
|
|
142
|
+
this.#via = Diagnostic.via(via);
|
|
143
|
+
if (readonly) {
|
|
144
|
+
this.#status = Status.ReadOnly;
|
|
145
|
+
} else {
|
|
146
|
+
this.#status = Status.Shared;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
close() {
|
|
151
|
+
this.#status = Status.Destroyed;
|
|
152
|
+
this.#resources.clear();
|
|
153
|
+
this.#roles.clear();
|
|
154
|
+
this.#participants.clear();
|
|
155
|
+
this.#closed?.emit();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get via() {
|
|
159
|
+
return this.#via;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get status() {
|
|
163
|
+
return this.#status;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
get participants() {
|
|
167
|
+
return this.#participants;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
get resources() {
|
|
171
|
+
return this.#resources;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
get waitingOn() {
|
|
175
|
+
return this.#waitingOn;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
get isAsync(): boolean {
|
|
179
|
+
return this.#isAsync;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
set isAsync(isAsync: true) {
|
|
183
|
+
// When the transaction is noted as async we start reporting locks. A further optimization would be to not even
|
|
184
|
+
// acquire locks for synchronous transactions
|
|
185
|
+
if (!this.#isAsync) {
|
|
186
|
+
this.#locksChanged(this.#resources);
|
|
187
|
+
}
|
|
188
|
+
this.#isAsync = isAsync;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
onShared(listener: () => void, once?: boolean) {
|
|
192
|
+
if (this.status === Status.ReadOnly) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.#shared === undefined) {
|
|
196
|
+
this.#shared = Observable();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this.#shared[once ? "once" : "on"](listener);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
onClose(listener: () => void) {
|
|
203
|
+
if (this.status === Status.ReadOnly) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (this.status === Status.Destroyed) {
|
|
207
|
+
listener();
|
|
208
|
+
}
|
|
209
|
+
if (this.#closed === undefined) {
|
|
210
|
+
this.#closed = Observable();
|
|
211
|
+
}
|
|
212
|
+
this.#closed.once(listener);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async addResources(...resources: Resource[]) {
|
|
216
|
+
this.#assertAvailable();
|
|
217
|
+
|
|
218
|
+
if (this.#status === Status.Exclusive) {
|
|
219
|
+
const set = new ResourceSet(this, resources);
|
|
220
|
+
const locked = await set.acquireLocks();
|
|
221
|
+
this.#locksChanged(locked);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.addResourcesSync(...resources);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
addResourcesSync(...resources: Resource[]) {
|
|
228
|
+
this.#assertAvailable();
|
|
229
|
+
|
|
230
|
+
if (this.#status === Status.Exclusive) {
|
|
231
|
+
const set = new ResourceSet(this, resources);
|
|
232
|
+
const locked = set.acquireLocksSync();
|
|
233
|
+
this.#locksChanged(locked);
|
|
234
|
+
} else if (this.#status !== Status.Shared) {
|
|
235
|
+
throw new TransactionFlowError(`Cannot add resources to transaction that is ${this.status}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const resource of resources) {
|
|
239
|
+
this.#resources.add(resource);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async begin() {
|
|
244
|
+
this.#assertAvailable();
|
|
245
|
+
|
|
246
|
+
if (this.status === Status.Exclusive) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (this.status !== Status.Shared) {
|
|
250
|
+
throw new TransactionFlowError(`Cannot begin write transaction because transaction is ${this.#status}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.#status = Status.Waiting;
|
|
254
|
+
try {
|
|
255
|
+
const resources = new ResourceSet(this, this.#resources);
|
|
256
|
+
const locked = await resources.acquireLocks();
|
|
257
|
+
this.#locksChanged(locked);
|
|
258
|
+
this.#status = Status.Exclusive;
|
|
259
|
+
} catch (e) {
|
|
260
|
+
this.#status = Status.Shared;
|
|
261
|
+
throw e;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
beginSync() {
|
|
266
|
+
this.#assertAvailable();
|
|
267
|
+
|
|
268
|
+
if (this.status === Status.Exclusive) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (this.status !== Status.Shared) {
|
|
272
|
+
throw new TransactionFlowError(`Cannot begin write transaction because transaction is ${this.#status}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.#status = Status.Exclusive;
|
|
276
|
+
try {
|
|
277
|
+
const resources = new ResourceSet(this, this.#resources);
|
|
278
|
+
const locked = resources.acquireLocksSync();
|
|
279
|
+
this.#locksChanged(locked);
|
|
280
|
+
} catch (e) {
|
|
281
|
+
this.#status = Status.Shared;
|
|
282
|
+
throw e;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
addParticipants(...participants: Participant[]) {
|
|
287
|
+
this.#assertAvailable();
|
|
288
|
+
|
|
289
|
+
for (const participant of participants) {
|
|
290
|
+
if (this.#participants.has(participant)) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// This sanity check uses the participant's diagnostic name to prevent logic errors from installing multiple
|
|
295
|
+
// participants that modify the same data
|
|
296
|
+
if ([...this.#participants].findIndex(p => p.toString() === participant.toString()) !== -1) {
|
|
297
|
+
throw new ImplementationError(`Participant ${participant} identity is not unique`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#participants.add(participant);
|
|
301
|
+
|
|
302
|
+
if (participant.role !== undefined) {
|
|
303
|
+
if (this.#roles.has(participant.role)) {
|
|
304
|
+
throw new TransactionFlowError(`A participant is already registered for role ${participant.role}`);
|
|
305
|
+
}
|
|
306
|
+
this.#roles.set(participant.role, participant);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getParticipant(role: {}) {
|
|
312
|
+
this.#assertAvailable();
|
|
313
|
+
|
|
314
|
+
return this.#roles.get(role);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
commit() {
|
|
318
|
+
this.#assertAvailable();
|
|
319
|
+
|
|
320
|
+
if (this.#status === Status.Shared) {
|
|
321
|
+
// Use rollback() to reset state
|
|
322
|
+
return this.rollback();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Perform the actual commit once preCommit completes
|
|
326
|
+
const performCommit = () => {
|
|
327
|
+
const participants = [...this.#participants];
|
|
328
|
+
const result = this.#finalize(Status.CommittingPhaseOne, "committed", this.#executeCommit.bind(this));
|
|
329
|
+
if (MaybePromise.is(result)) {
|
|
330
|
+
return result.then(() => this.#executePostCommit(participants));
|
|
331
|
+
}
|
|
332
|
+
return this.#executePostCommit(participants);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const result = this.#executePreCommit();
|
|
336
|
+
if (MaybePromise.is(result)) {
|
|
337
|
+
return result.then(performCommit);
|
|
338
|
+
}
|
|
339
|
+
return performCommit();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
rollback() {
|
|
343
|
+
this.#assertAvailable();
|
|
344
|
+
|
|
345
|
+
return this.#finalize(Status.RollingBack, "rolled back", () => this.#executeRollback());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
waitFor(others: Set<Transaction>) {
|
|
349
|
+
this.#assertAvailable();
|
|
350
|
+
|
|
351
|
+
if (this.waitingOn) {
|
|
352
|
+
throw new TransactionFlowError("Attempted wait on a transaction that is already waiting");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
logger.debug("Tx", this.via, "waiting on", describeList("and", ...[...others].map(other => other.via)));
|
|
356
|
+
|
|
357
|
+
this.#waitingOn = others;
|
|
358
|
+
return new Promise<void>(resolve => {
|
|
359
|
+
for (const other of others) {
|
|
360
|
+
other.onShared(() => {
|
|
361
|
+
others.delete(other);
|
|
362
|
+
if (!others.size) {
|
|
363
|
+
this.#waitingOn = undefined;
|
|
364
|
+
resolve();
|
|
365
|
+
}
|
|
366
|
+
}, true);
|
|
367
|
+
}
|
|
368
|
+
}).finally(() => (this.#waitingOn = undefined));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
toString() {
|
|
372
|
+
return `transaction<${this.via}>`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Shared implementation for commit and rollback.
|
|
377
|
+
*/
|
|
378
|
+
#finalize(status: Status, why: string, finalizer: () => MaybePromise) {
|
|
379
|
+
// Sanity check on status
|
|
380
|
+
if (this.status !== Status.Shared && this.status !== Status.Exclusive) {
|
|
381
|
+
throw new TransactionFlowError(
|
|
382
|
+
`Illegal attempt to enter status ${status} when transaction is ${this.#status}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Post-finalization state reset
|
|
387
|
+
const cleanup = () => {
|
|
388
|
+
// Release locks
|
|
389
|
+
const set = new ResourceSet(this, this.#resources);
|
|
390
|
+
const unlocked = set.releaseLocks();
|
|
391
|
+
this.#locksChanged(unlocked, `${why} and unlocked`);
|
|
392
|
+
|
|
393
|
+
// Release participants
|
|
394
|
+
this.#participants.clear();
|
|
395
|
+
|
|
396
|
+
// Revert to shared
|
|
397
|
+
this.#status = Status.Shared;
|
|
398
|
+
|
|
399
|
+
// Notify listeners
|
|
400
|
+
this.#shared?.emit();
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
// Perform the commit or rollback
|
|
404
|
+
let isAsync = false;
|
|
405
|
+
try {
|
|
406
|
+
this.#status = status;
|
|
407
|
+
const result = finalizer();
|
|
408
|
+
if (MaybePromise.is(result)) {
|
|
409
|
+
isAsync = true;
|
|
410
|
+
return Promise.resolve(result).finally(cleanup);
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
if (!isAsync) {
|
|
414
|
+
cleanup();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Iteratively execute pre-commit until all participants "settle" and report no possible mutation.
|
|
421
|
+
*/
|
|
422
|
+
#executePreCommit(): MaybePromise<void> {
|
|
423
|
+
let mayHaveMutated = false;
|
|
424
|
+
let abortedDueToError = false;
|
|
425
|
+
let iterator = this.participants[Symbol.iterator]();
|
|
426
|
+
let cycles = 1;
|
|
427
|
+
|
|
428
|
+
const errorRollback = (error?: any) => {
|
|
429
|
+
logger.error(
|
|
430
|
+
"Rolling back",
|
|
431
|
+
this.via,
|
|
432
|
+
"due to pre-commit error:",
|
|
433
|
+
Diagnostic.weak(error?.message || `${error}`),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const result = this.#finalize(Status.RollingBack, "rolled back", () => this.#executeRollback());
|
|
437
|
+
|
|
438
|
+
if (MaybePromise.is(result)) {
|
|
439
|
+
return result.then(() => {
|
|
440
|
+
throw error;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw error;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const nextCycle = () => {
|
|
448
|
+
// Guard against infinite loops
|
|
449
|
+
cycles++;
|
|
450
|
+
if (cycles > MAX_PRECOMMIT_CYCLES) {
|
|
451
|
+
return errorRollback(
|
|
452
|
+
new UnsettledStateError(
|
|
453
|
+
`State has not settled after ${MAX_PRECOMMIT_CYCLES} pre-commit cycles which likely indicates an infinite loop`,
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Restart iteration at the first participant
|
|
459
|
+
mayHaveMutated = false;
|
|
460
|
+
iterator = this.participants[Symbol.iterator]();
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const nextPreCommit = (previousResult?: boolean): MaybePromise<void> => {
|
|
464
|
+
// If an error occurred
|
|
465
|
+
if (abortedDueToError) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// When resuming after an async pre-commit handler, "previousResult" is the handler's return value
|
|
470
|
+
// indicating whether mutation may have occurred
|
|
471
|
+
if (previousResult) {
|
|
472
|
+
mayHaveMutated = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Cycle through participants until exhausted, there is an error or a pre-commit function is async
|
|
476
|
+
while (true) {
|
|
477
|
+
const n = iterator.next();
|
|
478
|
+
|
|
479
|
+
// If we've exhausted participants, we are either done or need to restart the cycle
|
|
480
|
+
if (n.done) {
|
|
481
|
+
// Restart the cycle if necessary
|
|
482
|
+
if (mayHaveMutated) {
|
|
483
|
+
const result = nextCycle();
|
|
484
|
+
if (MaybePromise.is(result)) {
|
|
485
|
+
return result;
|
|
486
|
+
}
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Done
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Process the next participant
|
|
495
|
+
const participant = n.value;
|
|
496
|
+
|
|
497
|
+
// When an error occurs this function performs rollback then throws
|
|
498
|
+
const handleError = (error: any) => {
|
|
499
|
+
abortedDueToError = true;
|
|
500
|
+
return errorRollback(error);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Execute the pre-commit for this participant
|
|
504
|
+
try {
|
|
505
|
+
const result = participant.preCommit?.();
|
|
506
|
+
if (MaybePromise.is(result)) {
|
|
507
|
+
return Promise.resolve(result).catch(handleError).then(nextPreCommit);
|
|
508
|
+
}
|
|
509
|
+
if (result) {
|
|
510
|
+
mayHaveMutated = true;
|
|
511
|
+
}
|
|
512
|
+
} catch (e) {
|
|
513
|
+
return handleError(e);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
return nextPreCommit();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Commit logic passed to #finalize.
|
|
523
|
+
*/
|
|
524
|
+
#executeCommit(): MaybePromise {
|
|
525
|
+
//this.#log("commit");
|
|
526
|
+
const result = this.#executeCommit1();
|
|
527
|
+
if (MaybePromise.is(result)) {
|
|
528
|
+
return Promise.resolve(result).then(this.#executeCommit2.bind(this));
|
|
529
|
+
}
|
|
530
|
+
return this.#executeCommit2();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
#executeCommit1(): MaybePromise {
|
|
534
|
+
// Commit phase 1
|
|
535
|
+
|
|
536
|
+
let needRollback = false;
|
|
537
|
+
let asyncCommits: undefined | Promise<void>[];
|
|
538
|
+
for (const participant of this.participants) {
|
|
539
|
+
const handleParticipantError = (error: any) => {
|
|
540
|
+
logger.error(`Error committing ${participant} (phase one):`, error);
|
|
541
|
+
needRollback = true;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
try {
|
|
545
|
+
const result = participant.commit1();
|
|
546
|
+
if (MaybePromise.is(result)) {
|
|
547
|
+
if (!asyncCommits) {
|
|
548
|
+
asyncCommits = [];
|
|
549
|
+
}
|
|
550
|
+
asyncCommits.push(Promise.resolve(result).catch(handleParticipantError));
|
|
551
|
+
}
|
|
552
|
+
} catch (e) {
|
|
553
|
+
handleParticipantError(e);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const abortIfFailed = () => {
|
|
559
|
+
if (needRollback) {
|
|
560
|
+
const result = this.#executeRollback();
|
|
561
|
+
|
|
562
|
+
if (MaybePromise.is(result)) {
|
|
563
|
+
return result.then(() => {
|
|
564
|
+
throw new FinalizationError("Rolled back due to commit phase one error");
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
throw new FinalizationError("Rolled back due to commit phase one error");
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
if (asyncCommits) {
|
|
573
|
+
return Promise.allSettled(asyncCommits).then(abortIfFailed);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return abortIfFailed();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
#executeCommit2() {
|
|
580
|
+
// Commit phase 2
|
|
581
|
+
this.#status = Status.CommittingPhaseTwo;
|
|
582
|
+
let errored: undefined | Array<Participant>;
|
|
583
|
+
let ongoing: undefined | Array<Promise<void>>;
|
|
584
|
+
for (const participant of this.participants) {
|
|
585
|
+
const promise = MaybePromise.then(
|
|
586
|
+
() => participant.commit2(),
|
|
587
|
+
undefined,
|
|
588
|
+
error => {
|
|
589
|
+
logger.error(`Error committing (phase two) ${participant}, state inconsistency possible:`, error);
|
|
590
|
+
|
|
591
|
+
if (errored) {
|
|
592
|
+
errored.push(participant);
|
|
593
|
+
} else {
|
|
594
|
+
errored = [participant];
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
if (MaybePromise.is(promise)) {
|
|
600
|
+
if (ongoing) {
|
|
601
|
+
ongoing.push(promise as Promise<void>);
|
|
602
|
+
} else {
|
|
603
|
+
ongoing = [promise as Promise<void>];
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
if (ongoing) {
|
|
609
|
+
// Async commit
|
|
610
|
+
return Promise.allSettled(ongoing).then(() => throwIfErrored(errored, "in commit phase 2"));
|
|
611
|
+
} else {
|
|
612
|
+
// Synchronous commit
|
|
613
|
+
throwIfErrored(errored, "in commit phase 2");
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
#executePostCommit(participants: Participant[]) {
|
|
618
|
+
const participantIterator = participants[Symbol.iterator]();
|
|
619
|
+
|
|
620
|
+
const postCommitNextParticipant = (): MaybePromise => {
|
|
621
|
+
const next = participantIterator.next();
|
|
622
|
+
|
|
623
|
+
if (next.done) {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const participant = next.value;
|
|
628
|
+
|
|
629
|
+
return MaybePromise.then(
|
|
630
|
+
() => participant.postCommit?.(),
|
|
631
|
+
() => postCommitNextParticipant(),
|
|
632
|
+
error => {
|
|
633
|
+
logger.error(`Error post-commit of ${participant}:`, error);
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
return postCommitNextParticipant();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Rollback logic passed to #finish.
|
|
643
|
+
*/
|
|
644
|
+
#executeRollback() {
|
|
645
|
+
this.#status = Status.RollingBack;
|
|
646
|
+
let errored: undefined | Array<Participant>;
|
|
647
|
+
let ongoing: undefined | Array<Promise<void>>;
|
|
648
|
+
|
|
649
|
+
for (const participant of this.participants) {
|
|
650
|
+
// Perform rollback
|
|
651
|
+
const promise = MaybePromise.then(
|
|
652
|
+
() => participant.rollback(),
|
|
653
|
+
undefined,
|
|
654
|
+
error => {
|
|
655
|
+
logger.error(`Error rolling back ${participant}, state inconsistency possible:`, error);
|
|
656
|
+
|
|
657
|
+
if (errored) {
|
|
658
|
+
errored.push(participant);
|
|
659
|
+
} else {
|
|
660
|
+
errored = [participant];
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// If commit is asynchronous, collect the promise
|
|
666
|
+
if (MaybePromise.is(promise)) {
|
|
667
|
+
if (ongoing) {
|
|
668
|
+
ongoing.push(promise as Promise<void>);
|
|
669
|
+
} else {
|
|
670
|
+
ongoing = [promise as Promise<void>];
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const finished = () => {
|
|
676
|
+
this.#status = Status.Shared;
|
|
677
|
+
throwIfErrored(errored, "in commit phase 2");
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
if (ongoing) {
|
|
681
|
+
// Async commit
|
|
682
|
+
return Promise.allSettled(ongoing).then(finished);
|
|
683
|
+
} else {
|
|
684
|
+
// Synchronous commit
|
|
685
|
+
finished();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
#locksChanged(resources: Set<Resource>, how = "locked") {
|
|
690
|
+
if (!resources.size || !this.isAsync) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
let resourceDescription;
|
|
695
|
+
if (how === "locked") {
|
|
696
|
+
resourceDescription = Diagnostic.strong(describeList("and", ...[...resources].map(r => r.toString())));
|
|
697
|
+
} else {
|
|
698
|
+
resourceDescription = `${resources.size} resource${resources.size === 1 ? "" : "s"}`;
|
|
699
|
+
}
|
|
700
|
+
logger.debug(this.via, how, resourceDescription);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
#assertAvailable() {
|
|
704
|
+
if (this.#status === Status.Destroyed) {
|
|
705
|
+
logger.warn(
|
|
706
|
+
"You have accessed transaction",
|
|
707
|
+
this.via,
|
|
708
|
+
"outside of the context in which it was active. Open a new context or ensure your operation completes before the context exits",
|
|
709
|
+
);
|
|
710
|
+
throw new TransactionDestroyedError(`Transaction ${this.#via} is destroyed`);
|
|
711
|
+
}
|
|
712
|
+
if (this.#status === Status.ReadOnly) {
|
|
713
|
+
throw new ReadOnlyError();
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* A read-only offline transaction you may use without context.
|
|
720
|
+
*/
|
|
721
|
+
export const ReadOnlyTransaction = new Tx("readonly", true);
|
|
722
|
+
|
|
723
|
+
function throwIfErrored(errored: undefined | Array<Participant>, when: string) {
|
|
724
|
+
if (!errored?.length) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const suffix = errored.length > 1 ? "s" : "";
|
|
728
|
+
throw new FinalizationError(
|
|
729
|
+
`Unhandled error${suffix} ${when} participant${suffix} ${describeList(
|
|
730
|
+
"and",
|
|
731
|
+
...errored.map(p => p.toString()),
|
|
732
|
+
)}`,
|
|
733
|
+
);
|
|
734
|
+
}
|