@photon-grove/evt 0.1.2 → 0.2.0
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/README.md +15 -3
- package/dist/dynamodb/head.d.ts +6 -0
- package/dist/dynamodb/head.d.ts.map +1 -1
- package/dist/dynamodb/head.js +11 -0
- package/dist/dynamodb/head.js.map +1 -1
- package/dist/dynamodb/index.d.ts +1 -1
- package/dist/dynamodb/index.d.ts.map +1 -1
- package/dist/dynamodb/index.js +1 -1
- package/dist/dynamodb/index.js.map +1 -1
- package/dist/dynamodb/repository.d.ts +9 -2
- package/dist/dynamodb/repository.d.ts.map +1 -1
- package/dist/dynamodb/repository.js +114 -35
- package/dist/dynamodb/repository.js.map +1 -1
- package/dist/dynamodb/requests.d.ts +36 -5
- package/dist/dynamodb/requests.d.ts.map +1 -1
- package/dist/dynamodb/requests.js +56 -13
- package/dist/dynamodb/requests.js.map +1 -1
- package/dist/errors.d.ts +38 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +45 -4
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/memory/repository.d.ts +3 -2
- package/dist/memory/repository.d.ts.map +1 -1
- package/dist/memory/repository.js +78 -29
- package/dist/memory/repository.js.map +1 -1
- package/dist/postgres/repository.d.ts +9 -2
- package/dist/postgres/repository.d.ts.map +1 -1
- package/dist/postgres/repository.js +107 -36
- package/dist/postgres/repository.js.map +1 -1
- package/dist/postgres/schema.d.ts.map +1 -1
- package/dist/postgres/schema.js +16 -0
- package/dist/postgres/schema.js.map +1 -1
- package/dist/repository.d.ts +75 -0
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +89 -3
- package/dist/repository.js.map +1 -1
- package/dist/staging.d.ts +28 -0
- package/dist/staging.d.ts.map +1 -0
- package/dist/staging.js +89 -0
- package/dist/staging.js.map +1 -0
- package/dist/store.d.ts +11 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +30 -71
- package/dist/store.js.map +1 -1
- package/dist/transaction.d.ts +113 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +207 -0
- package/dist/transaction.js.map +1 -0
- package/package.json +1 -1
- package/src/dynamodb/head.ts +15 -0
- package/src/dynamodb/index.ts +1 -0
- package/src/dynamodb/repository.ts +162 -41
- package/src/dynamodb/requests.ts +98 -14
- package/src/errors.ts +63 -4
- package/src/index.ts +16 -7
- package/src/memory/repository.ts +93 -32
- package/src/postgres/repository.ts +133 -48
- package/src/postgres/schema.ts +16 -0
- package/src/repository.ts +169 -2
- package/src/staging.ts +138 -0
- package/src/store.ts +47 -96
- package/src/transaction.ts +322 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// The Store's multi-stream transaction (ADR 0004 C1, C2): a read set of any aggregate types, the
|
|
2
|
+
// events decided for some of them, and one atomic commit that fences every stream it read but
|
|
3
|
+
// didn't write.
|
|
4
|
+
import { EvtError } from './errors.js';
|
|
5
|
+
import { emptyMetadata } from './metadata.js';
|
|
6
|
+
import { checkNotReserved, } from './repository.js';
|
|
7
|
+
import { commitSnapshot, copyMetadata, projectCommit, serializeEvents } from './staging.js';
|
|
8
|
+
import { compareUtf8 } from './utf8.js';
|
|
9
|
+
const MAX_INT64 = 2n ** 63n - 1n;
|
|
10
|
+
/**
|
|
11
|
+
* A stream read into a {@link Transaction}, typed by its aggregate. Pass it to
|
|
12
|
+
* {@link Transaction.append} to stage events for it; a read never appended to is fenced at the
|
|
13
|
+
* head it was read at.
|
|
14
|
+
*/
|
|
15
|
+
export class StreamRead {
|
|
16
|
+
#stream;
|
|
17
|
+
/** @internal Built by {@link Transaction.read}. */
|
|
18
|
+
constructor(stream) {
|
|
19
|
+
this.#stream = stream;
|
|
20
|
+
}
|
|
21
|
+
get entityId() {
|
|
22
|
+
return this.#stream.loaded.entityId;
|
|
23
|
+
}
|
|
24
|
+
/** The state, with the events appended through this handle. */
|
|
25
|
+
get state() {
|
|
26
|
+
return this.#stream.state;
|
|
27
|
+
}
|
|
28
|
+
/** The sequence of the last event, with the events appended through this handle. */
|
|
29
|
+
get sequence() {
|
|
30
|
+
return this.#stream.loaded.sequence + BigInt(this.#stream.events.length);
|
|
31
|
+
}
|
|
32
|
+
/** The stream as it was read. */
|
|
33
|
+
get loaded() {
|
|
34
|
+
return this.#stream.loaded;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One atomic commit across several streams. {@link read} loads streams of any aggregate types,
|
|
39
|
+
* {@link append} stages the events decided for them, {@link fence} adds a fence the caller stamped
|
|
40
|
+
* on a stream it didn't read, and {@link commit} writes everything in one repository transaction,
|
|
41
|
+
* or nothing. A stream read but never appended to adds a fence the engine filled at the head it
|
|
42
|
+
* was read at, so the commit fails with a retryable `FenceConflictError` once another writer
|
|
43
|
+
* moves it. Begin one with {@link Store.transaction}.
|
|
44
|
+
*
|
|
45
|
+
* Every staging call checks its input before it changes anything, so one that throws leaves the
|
|
46
|
+
* transaction as it was. A transaction commits once: after {@link commit}, whatever it returned
|
|
47
|
+
* or threw, every call throws `invalidCommit`. Begin a new transaction to try again.
|
|
48
|
+
*/
|
|
49
|
+
export class Transaction {
|
|
50
|
+
#context;
|
|
51
|
+
#streams = new Map();
|
|
52
|
+
// Every stream read or being read, by ID, with the position of its read call.
|
|
53
|
+
#ids = new Map();
|
|
54
|
+
#fences = new Map();
|
|
55
|
+
#reads = 0;
|
|
56
|
+
#pending = 0;
|
|
57
|
+
#done = false;
|
|
58
|
+
/** @internal Begun by {@link Store.transaction}. */
|
|
59
|
+
constructor(context) {
|
|
60
|
+
this.#context = context;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Loads a stream into the read set, as {@link Store.load} does, and returns its typed handle.
|
|
64
|
+
* Reading a stream the transaction already read, or is reading, throws `invalidCommit`.
|
|
65
|
+
*/
|
|
66
|
+
async read(aggregate, entityId, options = {}) {
|
|
67
|
+
this.#checkOpen();
|
|
68
|
+
if (this.#ids.has(entityId)) {
|
|
69
|
+
throw invalidCommit(`duplicate read of ${entityId}`);
|
|
70
|
+
}
|
|
71
|
+
// The ID is held while the load runs, so a concurrent read of the same stream is refused too,
|
|
72
|
+
// and the read keeps the place it was called in, whenever its load finishes.
|
|
73
|
+
const order = this.#reads++;
|
|
74
|
+
this.#ids.set(entityId, order);
|
|
75
|
+
this.#pending++;
|
|
76
|
+
let loaded;
|
|
77
|
+
try {
|
|
78
|
+
loaded = await this.#context.load(aggregate, entityId, options);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.#ids.delete(entityId);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
this.#pending--;
|
|
86
|
+
}
|
|
87
|
+
const stream = {
|
|
88
|
+
aggregate,
|
|
89
|
+
order,
|
|
90
|
+
loaded,
|
|
91
|
+
state: loaded.state,
|
|
92
|
+
events: [],
|
|
93
|
+
};
|
|
94
|
+
const read = new StreamRead(stream);
|
|
95
|
+
this.#streams.set(read, stream);
|
|
96
|
+
return read;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stages events for a stream this transaction read. They're applied to the handle's state, and
|
|
100
|
+
* appending again continues the sequence. The commit stamps their metadata, and runs the
|
|
101
|
+
* aggregate's projectors over every event appended to the stream, with its final state. A
|
|
102
|
+
* handle from another transaction throws `invalidCommit`.
|
|
103
|
+
*/
|
|
104
|
+
append(read, events) {
|
|
105
|
+
this.#checkOpen();
|
|
106
|
+
const stream = this.#streams.get(read);
|
|
107
|
+
if (stream === undefined) {
|
|
108
|
+
throw invalidCommit(`stream ${read.entityId} was not read by this transaction`);
|
|
109
|
+
}
|
|
110
|
+
// Serialized now, so an event the aggregate can't encode fails here, with nothing staged.
|
|
111
|
+
const after = stream.loaded.sequence + BigInt(stream.events.length);
|
|
112
|
+
const staged = serializeEvents(stream.aggregate, stream.loaded.entityId, after, stream.state, events, emptyMetadata());
|
|
113
|
+
stream.events.push(...staged.events);
|
|
114
|
+
stream.state = staged.state;
|
|
115
|
+
return this;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Adds a fence the caller stamped, for a stream the transaction doesn't read: the commit fails
|
|
119
|
+
* with a `FenceConflictError` once another writer moves the stream past `head`, which isn't
|
|
120
|
+
* retryable, since only the caller knows what it read. A fence on a stream the transaction reads,
|
|
121
|
+
* before or after the read, is dropped at commit: the read's own fence, or its write, holds the
|
|
122
|
+
* stream instead. A fence in a reserved namespace, at a head outside [0, 2^63 - 1), or on a
|
|
123
|
+
* stream already fenced throws, with nothing added.
|
|
124
|
+
*/
|
|
125
|
+
fence(fence) {
|
|
126
|
+
this.#checkOpen();
|
|
127
|
+
checkNotReserved(fence.entityId);
|
|
128
|
+
if (fence.head < 0n || fence.head >= MAX_INT64 || this.#fences.has(fence.entityId)) {
|
|
129
|
+
throw invalidCommit(`invalid or duplicate fence ${fence.entityId}`);
|
|
130
|
+
}
|
|
131
|
+
this.#fences.set(fence.entityId, { ...fence });
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Stamps one commit ID and the command's metadata on every appended event, and commits every
|
|
136
|
+
* stream with events, a fence on every stream read without any, and the caller's fences, in one
|
|
137
|
+
* atomic repository transaction. On failure nothing is written. A command ID that an event a
|
|
138
|
+
* read stream folded in since its latest snapshot already carries is a `duplicate`, and nothing
|
|
139
|
+
* is written. Projector operations follow the streams in the order they were read. A commit
|
|
140
|
+
* while a read is still loading throws `invalidCommit`, and leaves the transaction open.
|
|
141
|
+
*/
|
|
142
|
+
async commit(options = {}) {
|
|
143
|
+
this.#checkOpen();
|
|
144
|
+
if (this.#pending > 0) {
|
|
145
|
+
throw invalidCommit('a read is still loading; await it before the commit');
|
|
146
|
+
}
|
|
147
|
+
this.#done = true;
|
|
148
|
+
const metadata = this.#context.metadata(options.metadata);
|
|
149
|
+
const commandId = metadata.commandId;
|
|
150
|
+
const streams = [...this.#streams.values()];
|
|
151
|
+
streams.sort((a, b) => a.order - b.order);
|
|
152
|
+
if (commandId !== null &&
|
|
153
|
+
commandId !== '' &&
|
|
154
|
+
streams.some((stream) => stream.loaded.seenCommandIds.has(commandId))) {
|
|
155
|
+
return { kind: 'duplicate', commandId };
|
|
156
|
+
}
|
|
157
|
+
const commits = [];
|
|
158
|
+
const fences = [];
|
|
159
|
+
for (const stream of streams) {
|
|
160
|
+
const { aggregate, loaded } = stream;
|
|
161
|
+
if (stream.events.length === 0) {
|
|
162
|
+
fences.push({
|
|
163
|
+
entityId: loaded.entityId,
|
|
164
|
+
entityType: aggregate.type,
|
|
165
|
+
head: loaded.sequence,
|
|
166
|
+
provenance: 'engine',
|
|
167
|
+
});
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
for (const event of stream.events) {
|
|
171
|
+
event.metadata = copyMetadata(metadata);
|
|
172
|
+
}
|
|
173
|
+
const interval = this.#context.snapshotInterval(aggregate);
|
|
174
|
+
const appended = stream.events.length;
|
|
175
|
+
const snapshot = commitSnapshot(aggregate, loaded.entityId, interval, loaded, appended, stream.state);
|
|
176
|
+
commits.push({
|
|
177
|
+
entityId: loaded.entityId,
|
|
178
|
+
events: stream.events,
|
|
179
|
+
snapshot,
|
|
180
|
+
projections: projectCommit(aggregate, loaded.entityId, stream.state, stream.events, snapshot),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// A caller's fence on a stream the transaction read is superseded by the read's own.
|
|
184
|
+
for (const fence of this.#fences.values()) {
|
|
185
|
+
if (!this.#ids.has(fence.entityId)) {
|
|
186
|
+
fences.push({ ...fence, provenance: 'caller' });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
await this.#context.repository.commitTransaction({ streams: commits, fences }, options);
|
|
190
|
+
// Sorted apart from what the repository was given, which stays in read order.
|
|
191
|
+
const written = [...commits];
|
|
192
|
+
written.sort((a, b) => compareUtf8(a.entityId, b.entityId));
|
|
193
|
+
return {
|
|
194
|
+
kind: 'committed',
|
|
195
|
+
events: written.flatMap((commit) => commit.events),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
#checkOpen() {
|
|
199
|
+
if (this.#done) {
|
|
200
|
+
throw invalidCommit('the transaction has committed; begin a new one');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function invalidCommit(message) {
|
|
205
|
+
return new EvtError('invalidCommit', `invalid commit: ${message}`);
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/dynamodb/head.ts
CHANGED
|
@@ -72,6 +72,21 @@ export function headUpdate(
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** The condition of a fence on a stream with no head: it still has none. */
|
|
76
|
+
export const FENCE_ABSENT_CONDITION = 'attribute_not_exists(head)'
|
|
77
|
+
|
|
78
|
+
/** The condition of a fence on a stream with a head: it still equals the one observed. */
|
|
79
|
+
export const FENCE_CONDITION = 'head = :head'
|
|
80
|
+
|
|
81
|
+
/** The `ConditionCheck` a transaction fences a stream it read at `head` with. */
|
|
82
|
+
export function headFence(entityId: string, head: bigint): HeadAction {
|
|
83
|
+
const check = {entityId, updateExpression: null, names: []}
|
|
84
|
+
|
|
85
|
+
return head === 0n
|
|
86
|
+
? {...check, conditionExpression: FENCE_ABSENT_CONDITION, values: []}
|
|
87
|
+
: {...check, conditionExpression: FENCE_CONDITION, values: [[':head', head]]}
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
/**
|
|
76
91
|
* An action's size in the commit budget, by the rule for any update, delete or condition check:
|
|
77
92
|
* its key attributes plus its expression strings, name placeholders (the placeholder and the
|
package/src/dynamodb/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
ConflictError,
|
|
15
15
|
EvtError,
|
|
16
16
|
FeatureManifestChangedError,
|
|
17
|
+
FenceConflictError,
|
|
17
18
|
HistoryDependentProjectionError,
|
|
18
19
|
RebuildInProgressError,
|
|
19
20
|
SequenceGapError,
|
|
@@ -34,12 +35,17 @@ import {int64Argument} from '../fields.js'
|
|
|
34
35
|
import {
|
|
35
36
|
type CallOptions,
|
|
36
37
|
type Commit,
|
|
37
|
-
type
|
|
38
|
+
type NormalizedTransaction,
|
|
38
39
|
type StoreInitializer,
|
|
40
|
+
type TransactionCommit,
|
|
41
|
+
type TransactionalRepository,
|
|
39
42
|
checkBudget,
|
|
43
|
+
checkFenceRetention,
|
|
40
44
|
checkNotReserved,
|
|
41
|
-
commitSizes,
|
|
42
45
|
isReserved,
|
|
46
|
+
normalizeTransaction,
|
|
47
|
+
rankFailures,
|
|
48
|
+
transactionSizes,
|
|
43
49
|
validateCommit,
|
|
44
50
|
} from '../repository.js'
|
|
45
51
|
import type {DynamoClient, Page, RequestOptions, TransactItem} from './client.js'
|
|
@@ -57,10 +63,12 @@ import {FEATURES_PK, manifestFromItem, markerItem} from './marker.js'
|
|
|
57
63
|
import {DYNAMODB_BACKEND, transactItemSize} from './views.js'
|
|
58
64
|
import {
|
|
59
65
|
type CommitTtl,
|
|
60
|
-
|
|
66
|
+
type StreamLayout,
|
|
67
|
+
type TransactionLayout,
|
|
61
68
|
controlKey,
|
|
62
69
|
headKey,
|
|
63
70
|
projectorItems,
|
|
71
|
+
buildTransaction,
|
|
64
72
|
snapshotTransaction,
|
|
65
73
|
} from './requests.js'
|
|
66
74
|
|
|
@@ -121,7 +129,7 @@ const MAX_SNAPSHOT_GUARD_RETRIES = 4
|
|
|
121
129
|
* declares a feature, the next write fails, validates the manifest again, and either refuses an
|
|
122
130
|
* unsupported feature or fails once with a retryable {@link FeatureManifestChangedError}.
|
|
123
131
|
*/
|
|
124
|
-
export class DynamoRepository implements
|
|
132
|
+
export class DynamoRepository implements TransactionalRepository, StoreInitializer {
|
|
125
133
|
readonly #client: DynamoClient
|
|
126
134
|
readonly #table: string
|
|
127
135
|
readonly #lazy: boolean
|
|
@@ -187,29 +195,64 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
187
195
|
options?.signal?.throwIfAborted()
|
|
188
196
|
validateCommit(commit)
|
|
189
197
|
checkNotReserved(commit.entityId)
|
|
190
|
-
if (commit.events.length
|
|
191
|
-
|
|
198
|
+
if (commit.events.length > 0) {
|
|
199
|
+
await this.#write(normalizeTransaction({streams: [commit]}), options)
|
|
192
200
|
}
|
|
201
|
+
}
|
|
193
202
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Commits every stream and checks every fence in one `TransactWriteItems` call: the control
|
|
205
|
+
* checks, the projector operations, each written stream's actions, then a check on each fenced
|
|
206
|
+
* stream's head. A fence on a type this repository retains is refused before any request, and
|
|
207
|
+
* one on a type the store's manifest retains after the marker's validation.
|
|
208
|
+
*/
|
|
209
|
+
async commitTransaction(transaction: TransactionCommit, options?: CallOptions): Promise<void> {
|
|
210
|
+
options?.signal?.throwIfAborted()
|
|
211
|
+
const normalized = normalizeTransaction(transaction)
|
|
212
|
+
if (normalized.streams.length > 0 || normalized.fences.length > 0) {
|
|
213
|
+
await this.#write(normalized, options)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async #write(transaction: NormalizedTransaction, options?: CallOptions): Promise<void> {
|
|
218
|
+
// One `ttl` per entity type for the whole transaction, so a stream's head's is never below one
|
|
219
|
+
// of its own items.
|
|
220
|
+
const ttls = new Map<string, number | undefined>()
|
|
221
|
+
const ttlFor = (entityType: string): number | undefined => {
|
|
222
|
+
if (!ttls.has(entityType)) {
|
|
223
|
+
ttls.set(entityType, this.#ttlFor(entityType))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return ttls.get(entityType)
|
|
227
|
+
}
|
|
197
228
|
|
|
198
229
|
// Every backend budgets a commit the same way, so an oversized one fails before any request.
|
|
199
230
|
// The projector operations are sized from the actions this repository sends, which refuses a
|
|
200
231
|
// group for another backend here too.
|
|
201
|
-
const sizes = projectorItems(
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
232
|
+
const sizes = projectorItems(transaction.projections).map(transactItemSize)
|
|
233
|
+
const projections = [{backend: DYNAMODB_BACKEND, operations: sizes, sizes}]
|
|
234
|
+
checkBudget(transactionSizes({...transaction, projections}, ttlFor))
|
|
235
|
+
|
|
236
|
+
// A fence on a type this repository retains needs no request to refuse.
|
|
237
|
+
checkFenceRetention(transaction, (entityType) => this.#retention.has(entityType))
|
|
205
238
|
|
|
206
239
|
const request = requestOptions(options)
|
|
207
240
|
const version = await this.#markerVersion(request)
|
|
208
|
-
|
|
241
|
+
if (transaction.fences.length > 0) {
|
|
242
|
+
// The store's manifest may retain a type this repository doesn't. The marker check holds it
|
|
243
|
+
// at the version read here, or fails the transaction.
|
|
244
|
+
const manifest = await this.#readManifest(request)
|
|
245
|
+
const retention = manifest?.retention ?? []
|
|
246
|
+
checkFenceRetention(transaction, (entityType) => retention.includes(entityType))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const {items, layout} = buildTransaction(this.#table, transaction, version, (commit) =>
|
|
250
|
+
this.#commitTtl(commit, ttlFor(commit.events[0]!.entityType)),
|
|
251
|
+
)
|
|
209
252
|
try {
|
|
210
253
|
await this.#client.transactWriteItems({TransactItems: items}, request)
|
|
211
254
|
} catch (error) {
|
|
212
|
-
throw await this.#classifyCommit(
|
|
255
|
+
throw await this.#classifyCommit(layout, error, request)
|
|
213
256
|
}
|
|
214
257
|
}
|
|
215
258
|
|
|
@@ -297,6 +340,12 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
297
340
|
if (codes?.some((code) => THROTTLED.has(code))) {
|
|
298
341
|
throw new TransientError('the snapshot write was throttled', {cause: error})
|
|
299
342
|
}
|
|
343
|
+
if (codes?.includes('TransactionConflict')) {
|
|
344
|
+
throw new TransientError(
|
|
345
|
+
`transaction conflict on ${snapshot.entityId}: another writer was committing at the same time`,
|
|
346
|
+
{cause: error},
|
|
347
|
+
)
|
|
348
|
+
}
|
|
300
349
|
|
|
301
350
|
throw error
|
|
302
351
|
}
|
|
@@ -520,7 +569,14 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
520
569
|
return new FeatureManifestChangedError({cause})
|
|
521
570
|
}
|
|
522
571
|
|
|
523
|
-
|
|
572
|
+
// Maps a cancelled transaction to the error it reports. The control checks outrank everything.
|
|
573
|
+
// Then every failed stream and fence is classified, and the ranking reports the first drift, else
|
|
574
|
+
// the first gap, else the first other failure, in transaction order.
|
|
575
|
+
async #classifyCommit(
|
|
576
|
+
layout: TransactionLayout,
|
|
577
|
+
error: unknown,
|
|
578
|
+
request: RequestOptions,
|
|
579
|
+
): Promise<unknown> {
|
|
524
580
|
const codes = cancellationCodes(error)
|
|
525
581
|
if (codes === null) {
|
|
526
582
|
return error
|
|
@@ -528,10 +584,14 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
528
584
|
|
|
529
585
|
const failed = codes.indexOf(CONDITION_FAILED)
|
|
530
586
|
if (failed === -1) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
587
|
+
// Another transaction was writing one of the items, perhaps one this commit never changes
|
|
588
|
+
// (a control check): nothing was written and nothing is known to be stale, so it is
|
|
589
|
+
// transient, like throttling, and never a conflict, as in Go and Rust.
|
|
590
|
+
const colliding = codes.indexOf('TransactionConflict')
|
|
591
|
+
if (colliding !== -1) {
|
|
592
|
+
return new TransientError(
|
|
593
|
+
`transaction conflict on ${entityAt(layout, colliding)}: another writer was committing at the same time`,
|
|
594
|
+
{cause: error},
|
|
535
595
|
)
|
|
536
596
|
}
|
|
537
597
|
if (codes.some((code) => THROTTLED.has(code))) {
|
|
@@ -549,17 +609,57 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
549
609
|
return new RebuildInProgressError()
|
|
550
610
|
}
|
|
551
611
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
612
|
+
const failures: EvtError[] = []
|
|
613
|
+
for (const stream of layout.streams) {
|
|
614
|
+
const failure = await this.#streamFailure(stream, codes, request)
|
|
615
|
+
if (failure !== null) {
|
|
616
|
+
failures.push(failure)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const {index, fence} of layout.fences) {
|
|
620
|
+
if (codes[index] !== CONDITION_FAILED) {
|
|
621
|
+
continue
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const verdict = await this.#probe(fence.entityId, false, request)
|
|
625
|
+
failures.push(
|
|
626
|
+
verdict.drift ?? new FenceConflictError(fence.entityId, fence.head, fence.provenance),
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const ranked = rankFailures(failures)
|
|
631
|
+
if (ranked !== undefined) {
|
|
632
|
+
return ranked
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Only a projector operation's condition failed: its own error, as Go reports it.
|
|
636
|
+
if (failed < layout.projectorEnd) {
|
|
637
|
+
return error
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return new ConflictError(
|
|
641
|
+
entityAt(layout, failed),
|
|
642
|
+
'conditional check failed: the sequence or snapshot changed since it was read',
|
|
643
|
+
)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// What a written stream contributes to a failed transaction, or null when none of its actions
|
|
647
|
+
// failed. A failed head update is classified by the probe. A head update that passed while an
|
|
648
|
+
// event put failed is an event above the head, unless retention explains it. A failed snapshot
|
|
649
|
+
// put alone is an ordinary conflict.
|
|
650
|
+
async #streamFailure(
|
|
651
|
+
stream: StreamLayout,
|
|
652
|
+
codes: string[],
|
|
653
|
+
request: RequestOptions,
|
|
654
|
+
): Promise<EvtError | null> {
|
|
655
|
+
const {commit} = stream
|
|
558
656
|
const {entityId} = commit
|
|
559
657
|
const first = commit.events[0]!.sequence
|
|
560
658
|
const retained = this.#retention.has(commit.events[0]!.entityType)
|
|
659
|
+
const eventFailed = codes.slice(stream.head + 1, stream.eventsEnd).includes(CONDITION_FAILED)
|
|
660
|
+
const snapshotFailed = stream.snapshot !== null && codes[stream.snapshot] === CONDITION_FAILED
|
|
561
661
|
|
|
562
|
-
if (codes[head] === CONDITION_FAILED) {
|
|
662
|
+
if (codes[stream.head] === CONDITION_FAILED) {
|
|
563
663
|
const verdict = await this.#probe(entityId, retained, request)
|
|
564
664
|
if (verdict.drift) {
|
|
565
665
|
return verdict.drift
|
|
@@ -567,29 +667,26 @@ export class DynamoRepository implements Repository, StoreInitializer {
|
|
|
567
667
|
if (verdict.healthy && first > 1n && verdict.stored < first - 1n) {
|
|
568
668
|
return new SequenceGapError(entityId, verdict.stored, first)
|
|
569
669
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
// head
|
|
670
|
+
|
|
671
|
+
return conflict(entityId)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (eventFailed && codes[stream.head] === 'None') {
|
|
675
|
+
// With retention, TTL can delete the head before an expired event, so a create passes its
|
|
676
|
+
// head condition and fails on the leftover item: a conflict until TTL deletes the rest. The
|
|
677
|
+
// probe already read the remaining items when it found the head item absent below the head
|
|
678
|
+
// its log derives; read them only if it didn't.
|
|
576
679
|
const verdict = await this.#probe(entityId, retained, request)
|
|
577
680
|
const expired =
|
|
578
681
|
retained && (verdict.expired ?? (await this.#remainingItemsExpired(entityId, request)))
|
|
579
682
|
if (!expired) {
|
|
580
683
|
return verdict.drift ?? drift(entityId, verdict.head, verdict.derived)
|
|
581
684
|
}
|
|
582
|
-
}
|
|
583
685
|
|
|
584
|
-
|
|
585
|
-
if (failed < head && !codes.slice(head).includes(CONDITION_FAILED)) {
|
|
586
|
-
return error
|
|
686
|
+
return conflict(entityId)
|
|
587
687
|
}
|
|
588
688
|
|
|
589
|
-
return
|
|
590
|
-
entityId,
|
|
591
|
-
'conditional check failed: the sequence or snapshot changed since it was read',
|
|
592
|
-
)
|
|
689
|
+
return eventFailed || snapshotFailed ? conflict(entityId) : null
|
|
593
690
|
}
|
|
594
691
|
|
|
595
692
|
// Classifies a stream whose head update failed, as the failure-path probe does: a stored head
|
|
@@ -983,6 +1080,30 @@ interface Verdict {
|
|
|
983
1080
|
expired?: boolean
|
|
984
1081
|
}
|
|
985
1082
|
|
|
1083
|
+
function conflict(entityId: string): ConflictError {
|
|
1084
|
+
return new ConflictError(
|
|
1085
|
+
entityId,
|
|
1086
|
+
'conditional check failed: the sequence or snapshot changed since it was read',
|
|
1087
|
+
)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// The stream an action of a transaction belongs to, for a failure no stream check explains: the
|
|
1091
|
+
// written or fenced stream at that index, or else the transaction's first.
|
|
1092
|
+
function entityAt(layout: TransactionLayout, index: number): string {
|
|
1093
|
+
for (const stream of layout.streams) {
|
|
1094
|
+
if (index >= stream.head && (index < stream.eventsEnd || index === stream.snapshot)) {
|
|
1095
|
+
return stream.commit.entityId
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const fenced = layout.fences.find((each) => each.index === index)
|
|
1100
|
+
if (fenced !== undefined) {
|
|
1101
|
+
return fenced.fence.entityId
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return layout.streams[0]?.commit.entityId ?? layout.fences[0]?.fence.entityId ?? ''
|
|
1105
|
+
}
|
|
1106
|
+
|
|
986
1107
|
function drift(entityId: string, head: bigint | null, derived: bigint): StreamHeadDriftError {
|
|
987
1108
|
return new StreamHeadDriftError(entityId, head, derived)
|
|
988
1109
|
}
|