effect-dynamodb 1.34.1 → 1.36.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/dist/Aggregate.d.ts +1 -1
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +22 -6
- package/dist/Aggregate.js.map +1 -1
- package/dist/Batch.d.ts +1 -1
- package/dist/Batch.d.ts.map +1 -1
- package/dist/Batch.js +7 -5
- package/dist/Batch.js.map +1 -1
- package/dist/ConsumedCapacity.d.ts +107 -0
- package/dist/ConsumedCapacity.d.ts.map +1 -0
- package/dist/ConsumedCapacity.js +86 -0
- package/dist/ConsumedCapacity.js.map +1 -0
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +6 -1
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +1 -1
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +76 -42
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +78 -10
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +219 -35
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +15 -0
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +23 -8
- package/dist/Query.js.map +1 -1
- package/dist/Transaction.d.ts +1 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +7 -5
- package/dist/Transaction.js.map +1 -1
- package/dist/change-feed/ChangeFeed.d.ts.map +1 -1
- package/dist/change-feed/ChangeFeed.js +7 -4
- package/dist/change-feed/ChangeFeed.js.map +1 -1
- package/dist/change-feed/CheckpointStore.d.ts +1 -1
- package/dist/change-feed/CheckpointStore.d.ts.map +1 -1
- package/dist/change-feed/CheckpointStore.js +11 -3
- package/dist/change-feed/CheckpointStore.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/BoundCrud.js +3 -3
- package/dist/internal/BoundCrud.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +16 -0
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js +21 -4
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/BoundVectorQuery.d.ts +1 -1
- package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
- package/dist/internal/BoundVectorQuery.js +4 -3
- package/dist/internal/BoundVectorQuery.js.map +1 -1
- package/dist/internal/Capacity.d.ts +254 -0
- package/dist/internal/Capacity.d.ts.map +1 -0
- package/dist/internal/Capacity.js +653 -0
- package/dist/internal/Capacity.js.map +1 -0
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +4 -4
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +10 -1
- package/dist/internal/EntityOps.d.ts.map +1 -1
- package/dist/internal/EntityOps.js +22 -4
- package/dist/internal/EntityOps.js.map +1 -1
- package/dist/internal/EventProjection.d.ts.map +1 -1
- package/dist/internal/EventProjection.js +12 -8
- package/dist/internal/EventProjection.js.map +1 -1
- package/dist/internal/MultiProjection.d.ts.map +1 -1
- package/dist/internal/MultiProjection.js +11 -8
- package/dist/internal/MultiProjection.js.map +1 -1
- package/dist/internal/ProjectionEngine.d.ts.map +1 -1
- package/dist/internal/ProjectionEngine.js +5 -5
- package/dist/internal/ProjectionEngine.js.map +1 -1
- package/dist/internal/TransactPlan.d.ts.map +1 -1
- package/dist/internal/TransactPlan.js +5 -3
- package/dist/internal/TransactPlan.js.map +1 -1
- package/package.json +2 -2
package/dist/EventStore.js
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* state record as the load via `loadState`)
|
|
16
16
|
* - Head proofs (`append({ headProof })`, #164): an additional item or the
|
|
17
17
|
* inline snapshot proves the stream head in place of the version-contiguity
|
|
18
|
-
* `ConditionCheck`
|
|
18
|
+
* `ConditionCheck`, or (`headProof: "none"`, #173) the caller owns the head
|
|
19
|
+
* and no proof is written
|
|
20
|
+
* - A one-item append (its one event, nothing else) is a plain conditional
|
|
21
|
+
* `PutItem` rather than a transaction (#173)
|
|
19
22
|
* - Sealing (#166): `append(..., { seal: true })` makes its last event the
|
|
20
23
|
* stream's final one (`StreamSealed` for any later append), and
|
|
21
24
|
* `stream.appendOp(...)` carries one stream's append — sealing or not — in
|
|
@@ -45,11 +48,11 @@ import { Compression } from "@effect-dynamodb/schema/Compression.js";
|
|
|
45
48
|
import { isPackedConfig } from "@effect-dynamodb/schema/DynamoModel.js";
|
|
46
49
|
import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
|
|
47
50
|
import { normalizeTtlSeconds } from "@effect-dynamodb/schema/Entity.js";
|
|
48
|
-
import { AdditionalItemConditionFailed, AppendTooLarge, DuplicateCommand, isAwsConditionalCheckFailed, isAwsTransactionCancelled, StreamSealed, TransactionCancelled, ValidationError, VersionConflict, } from "@effect-dynamodb/schema/Errors.js";
|
|
51
|
+
import { AdditionalItemConditionFailed, AppendTooLarge, DuplicateCommand, isAwsConditionalCheckFailed, isAwsTransactionCancelled, isAwsValidationError, StreamSealed, TransactionCancelled, ValidationError, VersionConflict, } from "@effect-dynamodb/schema/Errors.js";
|
|
49
52
|
import { isDispatchEligible, packedShape, packValue, unpackStoredSync, } from "@effect-dynamodb/schema/internal/PackedCodec.js";
|
|
50
53
|
import * as KeyComposer from "@effect-dynamodb/schema/KeyComposer.js";
|
|
51
54
|
import { Context, DateTime, Effect, Function, Option, Pipeable, Result, Schedule, Schema, } from "effect";
|
|
52
|
-
import
|
|
55
|
+
import * as Capacity from "./internal/Capacity.js";
|
|
53
56
|
import { ProjectionRoutes } from "./internal/EventProjection.js";
|
|
54
57
|
import { eq } from "./internal/Expr.js";
|
|
55
58
|
import { itemBytes, transactItemBytes } from "./internal/ItemSize.js";
|
|
@@ -146,6 +149,37 @@ const PROJECTED_ENVELOPE_ATTRIBUTES = [
|
|
|
146
149
|
* every deployed `"withoutMetadata"` index.
|
|
147
150
|
*/
|
|
148
151
|
const SEALED_ATTRIBUTE = "__edd_sealed__";
|
|
152
|
+
/**
|
|
153
|
+
* @internal The write token of an event sent as a single-item `PutItem`
|
|
154
|
+
* (#173): random per `append` call, so the put can recognise its own item.
|
|
155
|
+
* A `TransactWriteItems` is idempotent under the AWS SDK's retries (the SDK
|
|
156
|
+
* fills its `ClientRequestToken`, and DynamoDB answers a resend of a
|
|
157
|
+
* committed transaction with success), but a `PutItem` has no token: a
|
|
158
|
+
* resend after a commit whose response was lost fails its own
|
|
159
|
+
* `attribute_not_exists(pk)`, and `ALL_OLD` returns the writer's own event.
|
|
160
|
+
* The token tells that case (success) from another writer's event at the
|
|
161
|
+
* same version (a conflict), however alike their content.
|
|
162
|
+
*/
|
|
163
|
+
const WRITE_TOKEN_ATTRIBUTE = "__edd_wt__";
|
|
164
|
+
/** @internal The write token's length in hex digits: 128 random bits. */
|
|
165
|
+
const WRITE_TOKEN_LENGTH = 32;
|
|
166
|
+
/** @internal A fresh write token, from the platform's Web Crypto (CSPRNG). */
|
|
167
|
+
const makeWriteToken = Effect.sync(() => Array.from(globalThis.crypto.getRandomValues(new Uint8Array(WRITE_TOKEN_LENGTH / 2)), (byte) => byte.toString(16).padStart(2, "0")).join(""));
|
|
168
|
+
/**
|
|
169
|
+
* @internal The event `Put` of a single-item append with `token` as its write
|
|
170
|
+
* token (see {@link WRITE_TOKEN_ATTRIBUTE}).
|
|
171
|
+
*/
|
|
172
|
+
const withWriteToken = (item, token) => ({
|
|
173
|
+
Put: {
|
|
174
|
+
...item.Put,
|
|
175
|
+
Item: { ...item.Put.Item, [WRITE_TOKEN_ATTRIBUTE]: { S: token } },
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
/**
|
|
179
|
+
* @internal Whether an append's request is exactly one item, its one event
|
|
180
|
+
* `Put` — sent as a `PutItem` (#173) rather than a `TransactWriteItems`.
|
|
181
|
+
*/
|
|
182
|
+
const isSingleEventPut = (items, eventCount) => items.length === 1 && eventCount === 1 && items[0].Put !== undefined;
|
|
149
183
|
/**
|
|
150
184
|
* @internal The version a sealed item returned by a failed condition
|
|
151
185
|
* (`ReturnValuesOnConditionCheckFailure: "ALL_OLD"`) seals the stream at: an
|
|
@@ -162,6 +196,51 @@ const sealedVersionOf = (item) => {
|
|
|
162
196
|
const version = typeof raw.version === "number" ? raw.version : raw.asOfVersion;
|
|
163
197
|
return typeof version === "number" ? version : undefined;
|
|
164
198
|
};
|
|
199
|
+
/**
|
|
200
|
+
* @internal A `PutItem` validation error that a transaction reports as its
|
|
201
|
+
* item's cancellation reason (`ValidationError`) rather than as a request
|
|
202
|
+
* error: a key whose type is not the table's or an index's ("Type mismatch
|
|
203
|
+
* for key", "Type mismatch for Index Key"). Observed on DynamoDB Local, where
|
|
204
|
+
* an oversized item, an empty or oversized index key are request errors of
|
|
205
|
+
* the transaction as of the `PutItem` — and named by the diagnosis on both —
|
|
206
|
+
* and an oversized table key is a cancellation reason the diagnosis names.
|
|
207
|
+
* The single-item fast path (#173) reports these as the transaction does.
|
|
208
|
+
*/
|
|
209
|
+
const ITEM_REASON_VALIDATION = /Type mismatch/i;
|
|
210
|
+
/**
|
|
211
|
+
* @internal The cancellation reason code a one-item `TransactWriteItems`
|
|
212
|
+
* reports for its item where a `PutItem` fails with `cause` instead, or
|
|
213
|
+
* `undefined` when the transaction reports it as a request error too (or not
|
|
214
|
+
* at all). The single-item fast path (#173) turns these into the
|
|
215
|
+
* `TransactionCancelled` the transaction gives:
|
|
216
|
+
*
|
|
217
|
+
* - `ProvisionedThroughputExceededException` — `ProvisionedThroughputExceeded`
|
|
218
|
+
* (a provisioned table's capacity);
|
|
219
|
+
* - `ThrottlingException` — `ThrottlingError`;
|
|
220
|
+
* - `ItemCollectionSizeLimitExceededException` —
|
|
221
|
+
* `ItemCollectionSizeLimitExceeded` (an LSI's 10 GB item collection, which
|
|
222
|
+
* a stream index is by default);
|
|
223
|
+
* - `TransactionConflictException` — `TransactionConflict`;
|
|
224
|
+
* - a key of the wrong type ({@link ITEM_REASON_VALIDATION}) —
|
|
225
|
+
* `ValidationError`.
|
|
226
|
+
*/
|
|
227
|
+
const putItemReason = (cause) => {
|
|
228
|
+
const name = cause?.name;
|
|
229
|
+
switch (name) {
|
|
230
|
+
case "ProvisionedThroughputExceededException":
|
|
231
|
+
return "ProvisionedThroughputExceeded";
|
|
232
|
+
case "ThrottlingException":
|
|
233
|
+
return "ThrottlingError";
|
|
234
|
+
case "ItemCollectionSizeLimitExceededException":
|
|
235
|
+
return "ItemCollectionSizeLimitExceeded";
|
|
236
|
+
case "TransactionConflictException":
|
|
237
|
+
return "TransactionConflict";
|
|
238
|
+
}
|
|
239
|
+
return isAwsValidationError(cause) &&
|
|
240
|
+
ITEM_REASON_VALIDATION.test(cause.message ?? "")
|
|
241
|
+
? "ValidationError"
|
|
242
|
+
: undefined;
|
|
243
|
+
};
|
|
165
244
|
/**
|
|
166
245
|
* @internal What a cancelled append means for one stream, from the reasons of
|
|
167
246
|
* its own items: `[0, guardCount)` the contiguity check,
|
|
@@ -221,6 +300,7 @@ const STREAM_OWNED_ATTRIBUTES = [
|
|
|
221
300
|
"state",
|
|
222
301
|
"commandId",
|
|
223
302
|
SEALED_ATTRIBUTE,
|
|
303
|
+
WRITE_TOKEN_ATTRIBUTE,
|
|
224
304
|
DEFAULT_TTL_ATTRIBUTE_NAME,
|
|
225
305
|
];
|
|
226
306
|
/** @internal Every member a stream index entry may declare (`StreamIndexConfigKey`). */
|
|
@@ -501,20 +581,20 @@ const snapshotProofRefusal = (streamName, settings, operation) => settings !== u
|
|
|
501
581
|
: `This stream's snapshot is mode "${settings.mode}"` +
|
|
502
582
|
`${settings.every === undefined ? "" : ` with every: ${settings.every}`}.`);
|
|
503
583
|
/**
|
|
504
|
-
* @internal Why a `headProof` is malformed — not `"snapshot"` nor
|
|
584
|
+
* @internal Why a `headProof` is malformed — not `"snapshot"`, `"none"` nor
|
|
505
585
|
* `{ additionalItem: n }` with `n` a non-negative integer — or `undefined`
|
|
506
586
|
* when it is well-formed.
|
|
507
587
|
*/
|
|
508
588
|
const headProofShapeProblem = (headProof) => {
|
|
509
|
-
if (headProof === "snapshot")
|
|
589
|
+
if (headProof === "snapshot" || headProof === "none")
|
|
510
590
|
return undefined;
|
|
511
591
|
const n = typeof headProof === "object" && headProof !== null
|
|
512
592
|
? headProof.additionalItem
|
|
513
593
|
: undefined;
|
|
514
594
|
return typeof n === "number" && Number.isInteger(n) && n >= 0
|
|
515
595
|
? undefined
|
|
516
|
-
: `headProof must be "snapshot" or { additionalItem: n } with n a non-negative
|
|
517
|
-
`received ${JSON.stringify(headProof) ?? String(headProof)}. Nothing was written.`;
|
|
596
|
+
: `headProof must be "snapshot", "none" or { additionalItem: n } with n a non-negative ` +
|
|
597
|
+
`integer; received ${JSON.stringify(headProof) ?? String(headProof)}. Nothing was written.`;
|
|
518
598
|
};
|
|
519
599
|
/**
|
|
520
600
|
* @internal Why an `eventTimestamps` value cannot be written for `count`
|
|
@@ -778,6 +858,12 @@ export const makeStream = (config) => {
|
|
|
778
858
|
*/
|
|
779
859
|
const codecCheck = runtimeCodecCheck(`EventStore (first use of stream "${config.streamName}")`, streamCodecRequirements(config.streamName, storage));
|
|
780
860
|
const checked = (op) => codecCheck === undefined ? op : (...args) => Effect.andThen(codecCheck, op(...args));
|
|
861
|
+
/**
|
|
862
|
+
* A stream method as a named operation for consumed-capacity reports
|
|
863
|
+
* (#174). Internal compositions call the unwrapped closures, so only the
|
|
864
|
+
* public method opens a scope.
|
|
865
|
+
*/
|
|
866
|
+
const named = (name, op) => (...args) => Capacity.operation(`EventStream.${name}`, { target: config.streamName }, op(...args));
|
|
781
867
|
/** The `[EDD-9076]` guard for queries of a stream whose `data` is packed. */
|
|
782
868
|
const dataGuards = dataStoredAs === undefined
|
|
783
869
|
? []
|
|
@@ -1005,12 +1091,14 @@ export const makeStream = (config) => {
|
|
|
1005
1091
|
*/
|
|
1006
1092
|
const encodeSnapshotState = (state, operation) => encodeToWire(snapshot.schema, state, operation, snapshotEntityType);
|
|
1007
1093
|
/**
|
|
1008
|
-
*
|
|
1094
|
+
* Send an append — see {@link AppendTransaction} for its layout. Resolves to
|
|
1009
1095
|
* the race a guarded additional put lost (so the transaction is built and
|
|
1010
1096
|
* written again), and fails with every other verdict.
|
|
1097
|
+
*
|
|
1098
|
+
* A request of one item, the append's one event, is a `PutItem` (#173): see
|
|
1099
|
+
* {@link sendAppendPut}. Everything else is one `TransactWriteItems`.
|
|
1011
1100
|
*/
|
|
1012
1101
|
const sendAppendTransaction = (client, tx, streamIdStr, commandId) => {
|
|
1013
|
-
const sent = () => ({ _tag: "Transaction", items: tx.items, targets: tx.targets });
|
|
1014
1102
|
const diagnosed = (diagnosis, original) => diagnosis._tag === "TooManyItems"
|
|
1015
1103
|
? new AppendTooLarge({
|
|
1016
1104
|
streamName: config.streamName,
|
|
@@ -1020,6 +1108,16 @@ export const makeStream = (config) => {
|
|
|
1020
1108
|
cause: original,
|
|
1021
1109
|
})
|
|
1022
1110
|
: appendLimitError(diagnosis, original, tx);
|
|
1111
|
+
// The stream's own key attributes and the additional items', for the
|
|
1112
|
+
// key-size diagnosis.
|
|
1113
|
+
const keys = () => mergeKeyAttributes({
|
|
1114
|
+
partition: ["pk", ...streamIndexes.map((idx) => idx.pk)],
|
|
1115
|
+
sort: ["sk", ...streamIndexes.map((idx) => idx.sk)],
|
|
1116
|
+
}, tx.additional.keys());
|
|
1117
|
+
if (isSingleEventPut(tx.items, tx.eventCount)) {
|
|
1118
|
+
return Effect.flatMap(makeWriteToken, (token) => sendAppendPut(client, tx, token, streamIdStr, keys, diagnosed));
|
|
1119
|
+
}
|
|
1120
|
+
const sent = () => ({ _tag: "Transaction", items: tx.items, targets: tx.targets });
|
|
1023
1121
|
return client.transactWriteItems({ TransactItems: tx.items }).pipe(Effect.as(undefined),
|
|
1024
1122
|
// One operation per item (#133, #144): diagnosed BEFORE the conditional
|
|
1025
1123
|
// mapping below — DynamoDB can report a repeated item as a conditional
|
|
@@ -1062,10 +1160,10 @@ export const makeStream = (config) => {
|
|
|
1062
1160
|
return Effect.fail(own);
|
|
1063
1161
|
// The head proof (#164) stands in for the contiguity check, so its
|
|
1064
1162
|
// own condition failing shares their verdict — ahead of the other
|
|
1065
|
-
// additional items.
|
|
1163
|
+
// additional items. `"none"` (#173) has no item to fail.
|
|
1066
1164
|
const proofFailed = tx.headProof === "snapshot"
|
|
1067
1165
|
? failedAt(tx.snapshotIndex)
|
|
1068
|
-
: tx.headProof
|
|
1166
|
+
: typeof tx.headProof === "object" &&
|
|
1069
1167
|
opConditionFailed(tx.additional, rawReasons, tx.headProof.additionalItem, tx.guardCount + tx.eventCount);
|
|
1070
1168
|
if (proofFailed) {
|
|
1071
1169
|
return Effect.fail(new VersionConflict({
|
|
@@ -1111,13 +1209,70 @@ export const makeStream = (config) => {
|
|
|
1111
1209
|
// transaction sent, naming the event, sentinel, snapshot or additional
|
|
1112
1210
|
// item at fault. Over 100 items is `AppendTooLarge`, with the EXPANDED
|
|
1113
1211
|
// count.
|
|
1212
|
+
diagnoseFailure({ sent, keys, rewrite: diagnosed }));
|
|
1213
|
+
};
|
|
1214
|
+
/**
|
|
1215
|
+
* The single-item fast path (#173): an append whose request is its one
|
|
1216
|
+
* event `Put` sends it as a `PutItem` — the same item, condition and
|
|
1217
|
+
* `ALL_OLD`, plus a write token (see {@link WRITE_TOKEN_ATTRIBUTE}) — at
|
|
1218
|
+
* 1 WCU per KB instead of a transaction's 2. One item commits atomically
|
|
1219
|
+
* either way, and every outcome maps to the one the transaction gives for
|
|
1220
|
+
* the same case:
|
|
1221
|
+
*
|
|
1222
|
+
* - a failed condition whose returned item carries this call's write token
|
|
1223
|
+
* is this call's own commit, met again by an SDK retry of a `PutItem`
|
|
1224
|
+
* whose response was lost: success, as the transaction's idempotent
|
|
1225
|
+
* retry is;
|
|
1226
|
+
* - any other failed condition is judged by {@link streamAppendVerdict}
|
|
1227
|
+
* from the one reason it stands for: `StreamSealed` when the returned
|
|
1228
|
+
* item is marked, `VersionConflict` otherwise;
|
|
1229
|
+
* - a rejection DynamoDB names (an item over 400 KB, a key too long or
|
|
1230
|
+
* empty) is diagnosed against the item sent and rewritten by the same
|
|
1231
|
+
* `rewrite`, so it names the event as the transaction's does;
|
|
1232
|
+
* - a failure a transaction reports as its item's cancellation reason is
|
|
1233
|
+
* `TransactionCancelled` with that reason (see {@link putItemReason}):
|
|
1234
|
+
* DynamoDB fails a `PutItem` with a request error where it cancels a
|
|
1235
|
+
* transaction for its item;
|
|
1236
|
+
* - anything else passes through, as it does from the transaction (the
|
|
1237
|
+
* other validation errors a one-item transaction gets are request
|
|
1238
|
+
* errors there too).
|
|
1239
|
+
*/
|
|
1240
|
+
const sendAppendPut = (client, tx, token, streamIdStr, keys, diagnosed) => {
|
|
1241
|
+
const item = withWriteToken(tx.items[0], token);
|
|
1242
|
+
return client.putItem(item.Put).pipe(Effect.as(undefined),
|
|
1243
|
+
// Diagnosed first: what it names is terminal, and what it cannot
|
|
1244
|
+
// explain is mapped below.
|
|
1114
1245
|
diagnoseFailure({
|
|
1115
|
-
sent,
|
|
1116
|
-
keys
|
|
1117
|
-
partition: ["pk", ...streamIndexes.map((idx) => idx.pk)],
|
|
1118
|
-
sort: ["sk", ...streamIndexes.map((idx) => idx.sk)],
|
|
1119
|
-
}, tx.additional.keys()),
|
|
1246
|
+
sent: () => ({ _tag: "Item", item, target: () => tx.targets()[0] }),
|
|
1247
|
+
keys,
|
|
1120
1248
|
rewrite: diagnosed,
|
|
1249
|
+
}), Effect.catch((error) => {
|
|
1250
|
+
if (error._tag === "AppendTooLarge" || error._tag === "ValidationError") {
|
|
1251
|
+
return Effect.fail(error);
|
|
1252
|
+
}
|
|
1253
|
+
const cause = error.cause;
|
|
1254
|
+
if (isAwsConditionalCheckFailed(cause)) {
|
|
1255
|
+
const returned = cause.Item;
|
|
1256
|
+
if (returned?.[WRITE_TOKEN_ATTRIBUTE]?.S === token)
|
|
1257
|
+
return Effect.succeed(undefined);
|
|
1258
|
+
const verdict = streamAppendVerdict([{ Code: "ConditionalCheckFailed", Item: returned }], {
|
|
1259
|
+
guardCount: 0,
|
|
1260
|
+
eventCount: 1,
|
|
1261
|
+
snapshotIndex: -1,
|
|
1262
|
+
streamName: config.streamName,
|
|
1263
|
+
streamId: streamIdStr,
|
|
1264
|
+
expectedVersion: tx.expectedVersion,
|
|
1265
|
+
});
|
|
1266
|
+
return Effect.fail(verdict ?? error);
|
|
1267
|
+
}
|
|
1268
|
+
const code = putItemReason(cause);
|
|
1269
|
+
if (code === undefined)
|
|
1270
|
+
return Effect.fail(error);
|
|
1271
|
+
return Effect.fail(new TransactionCancelled({
|
|
1272
|
+
operation: "PutItem",
|
|
1273
|
+
reasons: [{ code, message: cause.message }],
|
|
1274
|
+
cause,
|
|
1275
|
+
}));
|
|
1121
1276
|
}));
|
|
1122
1277
|
};
|
|
1123
1278
|
/**
|
|
@@ -1652,7 +1807,7 @@ export const makeStream = (config) => {
|
|
|
1652
1807
|
if (parts === undefined)
|
|
1653
1808
|
return { version: expectedVersion, events: [] };
|
|
1654
1809
|
const { tableName, streamIdStr, contiguityCheck, eventItems, sentinelPut, snapshotPut } = parts;
|
|
1655
|
-
const client = yield*
|
|
1810
|
+
const client = yield* Capacity.client;
|
|
1656
1811
|
/**
|
|
1657
1812
|
* Lay out the append's one transaction — see {@link AppendTransaction}:
|
|
1658
1813
|
* [0, C) version-contiguity ConditionCheck (C is 0 or 1)
|
|
@@ -1834,14 +1989,21 @@ export const makeStream = (config) => {
|
|
|
1834
1989
|
return { events: [], sentinel: undefined, snapshot: undefined, transactionBytes: 0 };
|
|
1835
1990
|
}
|
|
1836
1991
|
const putBytes = (item) => itemBytes(item.Put?.Item ?? {});
|
|
1837
|
-
const
|
|
1992
|
+
const laidOut = [
|
|
1838
1993
|
...(parts.contiguityCheck === undefined ? [] : [parts.contiguityCheck]),
|
|
1839
1994
|
...parts.eventItems,
|
|
1840
1995
|
...(parts.sentinelPut === undefined ? [] : [parts.sentinelPut]),
|
|
1841
1996
|
...(parts.snapshotPut === undefined ? [] : [parts.snapshotPut]),
|
|
1842
1997
|
];
|
|
1998
|
+
// A single-item append is sent as a `PutItem` carrying a write token
|
|
1999
|
+
// (#173), whose bytes are measured here as a placeholder of its length.
|
|
2000
|
+
// A `{ additionalItem }` proof means the append carries that item, so
|
|
2001
|
+
// it is a transaction.
|
|
2002
|
+
const single = typeof options?.headProof !== "object" && isSingleEventPut(laidOut, parts.eventItems.length);
|
|
2003
|
+
const owned = single ? [withWriteToken(laidOut[0], "0".repeat(WRITE_TOKEN_LENGTH))] : laidOut;
|
|
2004
|
+
const eventItems = single ? owned : parts.eventItems;
|
|
1843
2005
|
const measurement = {
|
|
1844
|
-
events:
|
|
2006
|
+
events: eventItems.map(putBytes),
|
|
1845
2007
|
sentinel: parts.sentinelPut === undefined ? undefined : putBytes(parts.sentinelPut),
|
|
1846
2008
|
snapshot: parts.snapshotPut === undefined ? undefined : putBytes(parts.snapshotPut),
|
|
1847
2009
|
transactionBytes: owned.reduce((sum, item) => sum + transactItemBytes(item), 0),
|
|
@@ -1940,7 +2102,7 @@ export const makeStream = (config) => {
|
|
|
1940
2102
|
const writeSnapshot = (streamId, state, asOfVersion) => Effect.gen(function* () {
|
|
1941
2103
|
if (snapshot === undefined)
|
|
1942
2104
|
return yield* snapshotUnavailable("writeSnapshot");
|
|
1943
|
-
const client = yield*
|
|
2105
|
+
const client = yield* Capacity.client;
|
|
1944
2106
|
const { name: tableName } = yield* config.table.Tag;
|
|
1945
2107
|
const now = DateTime.formatIso(yield* DateTime.now);
|
|
1946
2108
|
// Packed per `snapshot.storedAs` (#147): the diagnosis below measures
|
|
@@ -1994,7 +2156,7 @@ export const makeStream = (config) => {
|
|
|
1994
2156
|
})));
|
|
1995
2157
|
/** The raw snapshot item, by its exact key, in one `GetItem`. */
|
|
1996
2158
|
const getSnapshotItem = (streamId, consistentRead) => Effect.gen(function* () {
|
|
1997
|
-
const client = yield*
|
|
2159
|
+
const client = yield* Capacity.client;
|
|
1998
2160
|
const { name: tableName } = yield* config.table.Tag;
|
|
1999
2161
|
const result = yield* client.getItem({
|
|
2000
2162
|
TableName: tableName,
|
|
@@ -2008,7 +2170,7 @@ export const makeStream = (config) => {
|
|
|
2008
2170
|
});
|
|
2009
2171
|
/** The raw event item at `version`, by its exact key, in one `GetItem` (#166). */
|
|
2010
2172
|
const getEventItem = (streamId, version, consistentRead) => Effect.gen(function* () {
|
|
2011
|
-
const client = yield*
|
|
2173
|
+
const client = yield* Capacity.client;
|
|
2012
2174
|
const { name: tableName } = yield* config.table.Tag;
|
|
2013
2175
|
const result = yield* client.getItem({
|
|
2014
2176
|
TableName: tableName,
|
|
@@ -2084,7 +2246,7 @@ export const makeStream = (config) => {
|
|
|
2084
2246
|
}
|
|
2085
2247
|
}
|
|
2086
2248
|
}
|
|
2087
|
-
const client = yield*
|
|
2249
|
+
const client = yield* Capacity.client;
|
|
2088
2250
|
const { name: tableName } = yield* config.table.Tag;
|
|
2089
2251
|
// One reverse Query over `[first event SK, snapshot SK]`: the snapshot
|
|
2090
2252
|
// sorts after every event and the command sentinels before them (see
|
|
@@ -2180,7 +2342,7 @@ export const makeStream = (config) => {
|
|
|
2180
2342
|
// hasCommandSentinel (internal — see CommandSentinelProbe)
|
|
2181
2343
|
// ---------------------------------------------------------------------------
|
|
2182
2344
|
const hasCommandSentinel = (streamId, commandId) => Effect.gen(function* () {
|
|
2183
|
-
const client = yield*
|
|
2345
|
+
const client = yield* Capacity.client;
|
|
2184
2346
|
const { name: tableName } = yield* config.table.Tag;
|
|
2185
2347
|
const result = yield* client.getItem({
|
|
2186
2348
|
TableName: tableName,
|
|
@@ -2210,6 +2372,9 @@ export const makeStream = (config) => {
|
|
|
2210
2372
|
packed: dataGuards,
|
|
2211
2373
|
resolveTableName: config.table.Tag.useSync((tc) => tc.name),
|
|
2212
2374
|
keyFields: ["pk", "sk"],
|
|
2375
|
+
// Consumed capacity reports these queries under the stream name, as
|
|
2376
|
+
// every `EventStream.*` operation does (#174).
|
|
2377
|
+
operationTarget: config.streamName,
|
|
2213
2378
|
// Bound to the event SK range so non-event items in the stream partition
|
|
2214
2379
|
// (the snapshot) are excluded at the key-condition level. A caller-supplied
|
|
2215
2380
|
// `Query.where` replaces this — the `__edd_e__` filter still applies.
|
|
@@ -2290,6 +2455,7 @@ export const makeStream = (config) => {
|
|
|
2290
2455
|
label: `EventStream "${config.streamName}" index "${idx.name}"`,
|
|
2291
2456
|
},
|
|
2292
2457
|
keyFields: [idx.pk, idx.sk, "pk", "sk"],
|
|
2458
|
+
operationTarget: config.streamName,
|
|
2293
2459
|
// A "keys" index holds no events: an LSI has DynamoDB fetch them
|
|
2294
2460
|
// (`Select: ALL_ATTRIBUTES`), a GSI fetches each page's with
|
|
2295
2461
|
// `BatchGetItem` on the table key.
|
|
@@ -2393,15 +2559,15 @@ export const makeStream = (config) => {
|
|
|
2393
2559
|
snapshotConfig: snapshotSettings,
|
|
2394
2560
|
storedAs: storage,
|
|
2395
2561
|
indexes: indexSettings,
|
|
2396
|
-
writeSnapshot: checked(writeSnapshot),
|
|
2397
|
-
readSnapshot: checked(readSnapshot),
|
|
2398
|
-
append: checked(append),
|
|
2562
|
+
writeSnapshot: checked(named("writeSnapshot", writeSnapshot)),
|
|
2563
|
+
readSnapshot: checked(named("readSnapshot", readSnapshot)),
|
|
2564
|
+
append: checked(named("append", append)),
|
|
2399
2565
|
appendOp,
|
|
2400
|
-
read: checked(read),
|
|
2401
|
-
readFrom: checked(readFrom),
|
|
2402
|
-
currentVersion: checked(currentVersion),
|
|
2403
|
-
readLatest: checked(readLatest),
|
|
2404
|
-
readIndex: checked(readIndex),
|
|
2566
|
+
read: checked(named("read", read)),
|
|
2567
|
+
readFrom: checked(named("readFrom", readFrom)),
|
|
2568
|
+
currentVersion: checked(named("currentVersion", currentVersion)),
|
|
2569
|
+
readLatest: checked(named("readLatest", readLatest)),
|
|
2570
|
+
readIndex: checked(named("readIndex", readIndex)),
|
|
2405
2571
|
measureAppend: checked(measureAppend),
|
|
2406
2572
|
measureSnapshot: checked(measureSnapshot),
|
|
2407
2573
|
decodeStreamImage: checked(decodeStreamImage),
|
|
@@ -2429,7 +2595,8 @@ export const makeStream = (config) => {
|
|
|
2429
2595
|
* ```
|
|
2430
2596
|
*/
|
|
2431
2597
|
export const bind = (stream) => Effect.gen(function* () {
|
|
2432
|
-
|
|
2598
|
+
// Detached: no current operation or parent span is pinned for later calls (#174).
|
|
2599
|
+
const ctx = Capacity.detachContext(yield* Effect.context());
|
|
2433
2600
|
// The stream meets values compressed with every codec its `storedAs`
|
|
2434
2601
|
// names, so the registry captured here must have them (#147).
|
|
2435
2602
|
const codecError = unregisteredCodecError("EventStore.bind", streamCodecRequirements(stream.streamName, stream.storedAs), Context.getOrUndefined(ctx, Compression));
|
|
@@ -2440,6 +2607,7 @@ export const bind = (stream) => Effect.gen(function* () {
|
|
|
2440
2607
|
const headProbe = headAboveProbeOf(stream);
|
|
2441
2608
|
return {
|
|
2442
2609
|
[EventStreamTypeId]: EventStreamTypeId,
|
|
2610
|
+
[Capacity.CapturedReporting]: Capacity.capturedSettings(ctx),
|
|
2443
2611
|
pipe() {
|
|
2444
2612
|
// eslint-disable-next-line prefer-rest-params
|
|
2445
2613
|
return Pipeable.pipeArguments(this, arguments);
|
|
@@ -3007,6 +3175,17 @@ const makeCommandHandlerImpl = (decider, stream, options) => {
|
|
|
3007
3175
|
: stream.currentVersion(streamId, readOptions);
|
|
3008
3176
|
};
|
|
3009
3177
|
const attempt = Effect.gen(function* () {
|
|
3178
|
+
// `headProof: "none"` (#173) is `append`'s alone for now: refused for an
|
|
3179
|
+
// untyped caller before anything is read. Not retryable, as below.
|
|
3180
|
+
if (headProof === "none") {
|
|
3181
|
+
return yield* new ValidationError({
|
|
3182
|
+
entityType: stream.streamName,
|
|
3183
|
+
operation: "EventStore.commandHandler.headProof",
|
|
3184
|
+
cause: 'commandHandler does not accept headProof: "none": it is accepted by append only. ' +
|
|
3185
|
+
"Omit headProof to keep the contiguity check, or name a proof item. " +
|
|
3186
|
+
"Nothing was written.",
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3010
3189
|
// Backstop for JS callers and `any`-shaped call sites: silently degrading
|
|
3011
3190
|
// to at-least-once would look like success right up until the day a
|
|
3012
3191
|
// duplicate mattered. Not retryable — `while` below only retries
|
|
@@ -3143,7 +3322,7 @@ const makeCommandHandlerImpl = (decider, stream, options) => {
|
|
|
3143
3322
|
// answer, never a cue to re-read and re-decide. A conflict naming another
|
|
3144
3323
|
// stream (a nested `appendOp`, #166) is retried either way: the If-Match
|
|
3145
3324
|
// was not wrong, and the retried attempt checks it again on its reload.
|
|
3146
|
-
|
|
3325
|
+
const run = schedule === undefined
|
|
3147
3326
|
? attempt
|
|
3148
3327
|
: Effect.retry(attempt, {
|
|
3149
3328
|
// `while` runs before the schedule, so the schedule only ever sees a
|
|
@@ -3158,6 +3337,10 @@ const makeCommandHandlerImpl = (decider, stream, options) => {
|
|
|
3158
3337
|
while: (error) => error instanceof VersionConflict &&
|
|
3159
3338
|
(expectedVersion === undefined || !isOwnConflict(error)),
|
|
3160
3339
|
});
|
|
3340
|
+
// One report per invocation: the load and the append (and retries) nest
|
|
3341
|
+
// under it (#174). A bound stream's captured setting applies, as it does
|
|
3342
|
+
// to the bound calls inside.
|
|
3343
|
+
return Capacity.operation("EventStore.commandHandler", { target: stream.streamName }, run, stream[Capacity.CapturedReporting]);
|
|
3161
3344
|
};
|
|
3162
3345
|
};
|
|
3163
3346
|
/**
|
|
@@ -3316,7 +3499,8 @@ export { removeRow, skipEvent, } from "./internal/ProjectionEngine.js";
|
|
|
3316
3499
|
* ```
|
|
3317
3500
|
*/
|
|
3318
3501
|
export const bindProjection = (projection) => Effect.gen(function* () {
|
|
3319
|
-
|
|
3502
|
+
// Detached: no current operation or parent span is pinned for later calls (#174).
|
|
3503
|
+
const ctx = Capacity.detachContext(yield* Effect.context());
|
|
3320
3504
|
// The codec check `bind` makes, for every stream, against the registry captured here.
|
|
3321
3505
|
const streams = isMultiProjection(projection)
|
|
3322
3506
|
? Object.values(projection.sources).map((source) => source.stream)
|