effect-dynamodb 1.23.0 → 1.25.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 +5 -2
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +136 -53
- package/dist/Aggregate.js.map +1 -1
- package/dist/Batch.d.ts +14 -5
- package/dist/Batch.d.ts.map +1 -1
- package/dist/Batch.js +184 -119
- package/dist/Batch.js.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +2 -4
- package/dist/Collection.js.map +1 -1
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +42 -14
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +19 -13
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +287 -226
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +1060 -50
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +1607 -299
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +47 -14
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +190 -68
- package/dist/Query.js.map +1 -1
- package/dist/Table.d.ts +56 -6
- package/dist/Table.d.ts.map +1 -1
- package/dist/Table.js +134 -54
- package/dist/Table.js.map +1 -1
- package/dist/Transaction.d.ts +8 -0
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +67 -22
- package/dist/Transaction.js.map +1 -1
- package/dist/VectorSearchEmulation.d.ts +3 -2
- package/dist/VectorSearchEmulation.d.ts.map +1 -1
- package/dist/VectorSearchEmulation.js +14 -2
- package/dist/VectorSearchEmulation.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +3 -2
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/BoundVectorQuery.d.ts +5 -1
- package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
- package/dist/internal/BoundVectorQuery.js +17 -5
- package/dist/internal/BoundVectorQuery.js.map +1 -1
- package/dist/internal/EntityCombinators.d.ts +8 -5
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +8 -5
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +1 -1
- package/dist/internal/Expr.d.ts +4 -0
- package/dist/internal/Expr.d.ts.map +1 -1
- package/dist/internal/Expr.js +6 -4
- package/dist/internal/Expr.js.map +1 -1
- package/dist/internal/ExpressionLimits.d.ts +45 -0
- package/dist/internal/ExpressionLimits.d.ts.map +1 -0
- package/dist/internal/ExpressionLimits.js +89 -0
- package/dist/internal/ExpressionLimits.js.map +1 -0
- package/dist/internal/Hydrate.d.ts +62 -0
- package/dist/internal/Hydrate.d.ts.map +1 -0
- package/dist/internal/Hydrate.js +191 -0
- package/dist/internal/Hydrate.js.map +1 -0
- package/dist/internal/ItemSize.d.ts +60 -24
- package/dist/internal/ItemSize.d.ts.map +1 -1
- package/dist/internal/ItemSize.js +122 -39
- package/dist/internal/ItemSize.js.map +1 -1
- package/dist/internal/TransactWriteOps.d.ts +22 -46
- package/dist/internal/TransactWriteOps.d.ts.map +1 -1
- package/dist/internal/TransactWriteOps.js +26 -95
- package/dist/internal/TransactWriteOps.js.map +1 -1
- package/dist/internal/WriteDiagnosis.d.ts +353 -0
- package/dist/internal/WriteDiagnosis.d.ts.map +1 -0
- package/dist/internal/WriteDiagnosis.js +1028 -0
- package/dist/internal/WriteDiagnosis.js.map +1 -0
- package/dist/internal/sparse_check.js +43 -79
- package/dist/internal/sparse_check.js.map +1 -1
- package/package.json +2 -2
package/dist/EventStore.d.ts
CHANGED
|
@@ -4,18 +4,27 @@
|
|
|
4
4
|
* Provides:
|
|
5
5
|
* - `Decider` type for command-event-state modeling
|
|
6
6
|
* - `makeStream` factory for creating event streams bound to a Table
|
|
7
|
-
* - Core operations: `append
|
|
8
|
-
*
|
|
9
|
-
* - `
|
|
10
|
-
*
|
|
7
|
+
* - Core operations: `append` (atomic), `read`, `readFrom`, `currentVersion`,
|
|
8
|
+
* `readLatest` (reads optionally strongly consistent)
|
|
9
|
+
* - Snapshot primitives: `writeSnapshot`, `readSnapshot`, and inline snapshots
|
|
10
|
+
* written in the append transaction (`AppendOptions.snapshot`)
|
|
11
|
+
* - `commandHandler` combinator for the load-decide-fold-append cycle
|
|
12
|
+
* (snapshot-aware single-request loads, consistent loads, an optional
|
|
13
|
+
* `VersionConflict` retry policy, caller-supplied `expectedVersion`,
|
|
14
|
+
* decision-derived `additionalItems`, inline snapshots)
|
|
15
|
+
* - Stream indexes (`makeStream({ indexes })`): sub-streams ordered by a key
|
|
16
|
+
* derived from each event, on an LSI or GSI, each projecting all of the
|
|
17
|
+
* event, all but its metadata, or only its keys — `readIndex`,
|
|
18
|
+
* `query.index`, and `indexDefinitions` for the `CreateTable` fragments
|
|
11
19
|
* - `fold` / `foldFrom` helpers for state reconstruction
|
|
12
20
|
*
|
|
13
21
|
* Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
|
|
14
22
|
* DynamoClient, Marshaller).
|
|
15
23
|
*/
|
|
24
|
+
import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
|
|
16
25
|
import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
|
|
17
26
|
import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
|
|
18
|
-
import { type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
|
|
27
|
+
import { DateTime, type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
|
|
19
28
|
import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
|
|
20
29
|
import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
|
|
21
30
|
import * as Query from "./Query.js";
|
|
@@ -24,8 +33,13 @@ import { type Table, type TableConfig } from "./Table.js";
|
|
|
24
33
|
* A Decider encodes the command-event-state triad for an aggregate.
|
|
25
34
|
*
|
|
26
35
|
* - `decide` — given a command and current state, produce events (or fail with E)
|
|
27
|
-
* - `evolve` —
|
|
28
|
-
*
|
|
36
|
+
* - `evolve` — left fold: apply one event to a state. It may return new state or
|
|
37
|
+
* mutate the given state in place and return it; the library requires
|
|
38
|
+
* neither. State the library persists, returns or projects is always this
|
|
39
|
+
* fold, never anything produced inside `decide`.
|
|
40
|
+
* - `initialState` — starting state for a new aggregate. An `evolve` that
|
|
41
|
+
* mutates in place must not mutate a shared `initialState` object (declare it
|
|
42
|
+
* as a getter that returns a fresh value).
|
|
29
43
|
*/
|
|
30
44
|
export interface Decider<State, Command, Event, E = never> {
|
|
31
45
|
readonly decide: (command: Command, state: State) => Effect.Effect<ReadonlyArray<Event>, E>;
|
|
@@ -45,6 +59,11 @@ export interface StreamEvent<A, M = Record<string, unknown> | undefined> {
|
|
|
45
59
|
readonly eventType: string;
|
|
46
60
|
readonly data: A;
|
|
47
61
|
readonly metadata: M;
|
|
62
|
+
/**
|
|
63
|
+
* ISO 8601 (UTC): when the event was appended, by the append's `Clock` — or
|
|
64
|
+
* the `eventTimestamps` entry it was appended with
|
|
65
|
+
* ({@link AppendOptionsBase.eventTimestamps}).
|
|
66
|
+
*/
|
|
48
67
|
readonly timestamp: string;
|
|
49
68
|
}
|
|
50
69
|
/**
|
|
@@ -80,16 +99,52 @@ export interface Snapshot<State> {
|
|
|
80
99
|
* - `schema` — the state codec. Snapshot state round-trips through it
|
|
81
100
|
* (`Schema.encodeUnknownEffect` on write, `Schema.decodeUnknownEffect` on
|
|
82
101
|
* read), so transforming schemas work.
|
|
83
|
-
* - `
|
|
84
|
-
*
|
|
85
|
-
*
|
|
102
|
+
* - `mode` — how {@link commandHandler} writes snapshots (see
|
|
103
|
+
* {@link SnapshotMode}). Default `"after-append"`.
|
|
104
|
+
* - `every` — optional snapshot cadence for {@link commandHandler}: write a
|
|
105
|
+
* fresh snapshot once at least this many events have accumulated since the
|
|
106
|
+
* last one. Must be a positive integer. With `mode: "after-append"` and no
|
|
107
|
+
* `every`, the handler never snapshots; with `mode: "inline"` and no `every`,
|
|
108
|
+
* it snapshots on every append.
|
|
109
|
+
*
|
|
110
|
+
* `every` also sizes the first page of {@link EventStream.readLatest}, which
|
|
111
|
+
* {@link commandHandler} loads state with: a snapshot that lags the head by up
|
|
112
|
+
* to `every` events still loads in one request. The first page is read whatever
|
|
113
|
+
* the actual lag, so every load reads up to `every + 1` items (`2` without
|
|
114
|
+
* `every`), including events the snapshot already covers, which are discarded.
|
|
115
|
+
* For an `"after-append"` stream that trades read capacity for requests: with
|
|
116
|
+
* `every: 100` and a snapshot one event behind, a load reads the snapshot and
|
|
117
|
+
* 100 events in one request, where a `GetItem` plus `readFrom` would read the
|
|
118
|
+
* snapshot and one event in two. Size `every` with that in mind, or use
|
|
119
|
+
* `mode: "inline"`, which keeps the snapshot current so the page stays small.
|
|
86
120
|
*/
|
|
87
121
|
export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
|
|
122
|
+
/**
|
|
123
|
+
* The state schema. Snapshot state is encoded through it, falling back to
|
|
124
|
+
* `decode → encode` (as events are) for a value not already in its type
|
|
125
|
+
* shape — so a `Schema.Class` state folded by an immutable `evolve` that
|
|
126
|
+
* spreads (`({ ...s, balance })`, a plain object) still encodes.
|
|
127
|
+
*/
|
|
88
128
|
readonly schema: TSchema;
|
|
129
|
+
readonly mode?: SnapshotMode | undefined;
|
|
89
130
|
readonly every?: number | undefined;
|
|
90
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* How {@link commandHandler} writes snapshots.
|
|
134
|
+
*
|
|
135
|
+
* - `"after-append"` (default) — a best-effort `writeSnapshot` after the append
|
|
136
|
+
* succeeds, at the `every` cadence. A failed snapshot write never fails the
|
|
137
|
+
* command; the snapshot simply lags until the next threshold crossing.
|
|
138
|
+
* - `"inline"` — the snapshot `Put` rides in the append's own transaction (see
|
|
139
|
+
* {@link AppendOptionsBase.snapshot}), on every append, or at the `every` cadence
|
|
140
|
+
* when one is set. The snapshot is then current after every command that
|
|
141
|
+
* writes one, so {@link EventStream.readLatest} loads state in one request.
|
|
142
|
+
* The snapshot counts towards the transaction's item and size limits.
|
|
143
|
+
*/
|
|
144
|
+
export type SnapshotMode = "after-append" | "inline";
|
|
91
145
|
/** The runtime snapshot settings exposed on a stream. */
|
|
92
146
|
export interface SnapshotSettings {
|
|
147
|
+
readonly mode: SnapshotMode;
|
|
93
148
|
readonly every: number | undefined;
|
|
94
149
|
}
|
|
95
150
|
/**
|
|
@@ -100,6 +155,12 @@ export interface SnapshotSettings {
|
|
|
100
155
|
* `commandId` therefore cancels the whole transaction and surfaces as
|
|
101
156
|
* `DuplicateCommand` — the events are never written twice.
|
|
102
157
|
*
|
|
158
|
+
* The sentinel is consulted only by an append. Through `commandHandler`, a
|
|
159
|
+
* redelivery whose `decide` returns no events against the loaded state never
|
|
160
|
+
* appends, so it succeeds as a no-op instead of failing with
|
|
161
|
+
* `DuplicateCommand`. Whether a no-op redelivery needs to be told apart is the
|
|
162
|
+
* application's call (see {@link CommandOptionsBase.commandId}).
|
|
163
|
+
*
|
|
103
164
|
* The sentinel is co-located in the stream's own partition, so `commandId`
|
|
104
165
|
* uniqueness is scoped to the stream (which is what "have I already applied this
|
|
105
166
|
* command to this aggregate?" asks). It is invisible to `read` / `readFrom` /
|
|
@@ -122,10 +183,47 @@ export interface AppendIdempotency {
|
|
|
122
183
|
*/
|
|
123
184
|
readonly ttl?: Duration.Duration | string;
|
|
124
185
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The metadata of an {@link EventStream.append}: one value shared by every
|
|
188
|
+
* event (`metadata`), or one entry per event (`eventMetadata`) — never both.
|
|
189
|
+
*
|
|
190
|
+
* Passing both is a compile error, and a `ValidationError` at runtime for an
|
|
191
|
+
* untyped caller. `undefined` counts as absent for either, so
|
|
192
|
+
* `{ metadata: undefined, eventMetadata: [...] }` is not "both".
|
|
193
|
+
*/
|
|
194
|
+
export type AppendMetadataOptions<TMetadata> = {
|
|
195
|
+
/**
|
|
196
|
+
* Metadata written onto **every** event of the append — a copy per item,
|
|
197
|
+
* encoded once through the stream's metadata schema when one is
|
|
198
|
+
* configured. For metadata that belongs to each event (an event id, the
|
|
199
|
+
* original occurrence time of an imported event) use `eventMetadata`:
|
|
200
|
+
* packing a list into `metadata` stores every entry on every item, so
|
|
201
|
+
* stored metadata grows with the square of the append size.
|
|
202
|
+
*/
|
|
128
203
|
readonly metadata?: TMetadata;
|
|
204
|
+
readonly eventMetadata?: undefined;
|
|
205
|
+
} | {
|
|
206
|
+
readonly metadata?: undefined;
|
|
207
|
+
/**
|
|
208
|
+
* Per-event metadata: entry `i` is written onto event `i` (and read back
|
|
209
|
+
* as that event's `StreamEvent.metadata`). Each defined entry is encoded
|
|
210
|
+
* through the stream's metadata schema, as `metadata` is; an `undefined`
|
|
211
|
+
* entry writes its event without a `metadata` attribute.
|
|
212
|
+
*
|
|
213
|
+
* Must have exactly one entry per event — a length mismatch fails with
|
|
214
|
+
* `ValidationError` before anything is read or written, also for an
|
|
215
|
+
* append of no events (where only `[]` is accepted).
|
|
216
|
+
*/
|
|
217
|
+
readonly eventMetadata?: ReadonlyArray<TMetadata | undefined>;
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* The options of {@link EventStream.append} other than its metadata (see
|
|
221
|
+
* {@link AppendMetadataOptions}).
|
|
222
|
+
*
|
|
223
|
+
* `TState` is the stream's snapshot state type (`never` for a stream without a
|
|
224
|
+
* `snapshot` config, so `snapshot` cannot be supplied there).
|
|
225
|
+
*/
|
|
226
|
+
export interface AppendOptionsBase<TState = never> {
|
|
129
227
|
/**
|
|
130
228
|
* Caller-owned transact items committed atomically with the events — the same
|
|
131
229
|
* op union `Transaction.transactWrite` accepts (`EntityPut`, `EntityDelete`,
|
|
@@ -147,9 +245,305 @@ export interface AppendOptions<TMetadata> {
|
|
|
147
245
|
readonly additionalItems?: ReadonlyArray<TransactWriteOp>;
|
|
148
246
|
/** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
|
|
149
247
|
readonly idempotency?: AppendIdempotency;
|
|
248
|
+
/**
|
|
249
|
+
* An inline snapshot: the stream's state as of this append's last event.
|
|
250
|
+
*
|
|
251
|
+
* The snapshot `Put` (`asOfVersion` = `expectedVersion + events.length`,
|
|
252
|
+
* encoded through the state schema) joins the **same transaction** as the
|
|
253
|
+
* events, after the idempotency sentinel, so it commits if and only if the
|
|
254
|
+
* events do. The `Put` is unconditional: the event puts already prove this
|
|
255
|
+
* writer owns `asOfVersion`, so it cannot regress the snapshot. It counts
|
|
256
|
+
* towards the item and size limits, and a cancellation reason at its
|
|
257
|
+
* position (throttling, for example) reports `TransactionCancelled`.
|
|
258
|
+
*
|
|
259
|
+
* `undefined` means no snapshot. Requires at least one event — with none,
|
|
260
|
+
* nothing proves the writer owns the version, and the append fails with
|
|
261
|
+
* `ValidationError`. On a stream without a `snapshot` config (where the type
|
|
262
|
+
* is `never`) it dies with `[EDD-9026]`, as `writeSnapshot` does.
|
|
263
|
+
*/
|
|
264
|
+
readonly snapshot?: TState;
|
|
265
|
+
/**
|
|
266
|
+
* Per-event timestamps: entry `i` is written as event `i`'s `timestamp` (and
|
|
267
|
+
* read back as that event's `StreamEvent.timestamp`), for history imported
|
|
268
|
+
* with its original occurrence times. An `undefined` entry keeps the
|
|
269
|
+
* append's `Clock` time. Independent of `metadata` / `eventMetadata`.
|
|
270
|
+
*
|
|
271
|
+
* Written with `DateTime.formatIso` — the format the append's own time is
|
|
272
|
+
* written in — so a zoned `DateTime` is stored as its UTC instant.
|
|
273
|
+
*
|
|
274
|
+
* - Must have exactly one entry per event, each a valid `DateTime` (finite
|
|
275
|
+
* epoch) or `undefined` — anything else fails with `ValidationError` before anything is read or
|
|
276
|
+
* written, also for an append of no events (where only `[]` is accepted).
|
|
277
|
+
* - Only the event items change: the idempotency sentinel, its TTL and the
|
|
278
|
+
* inline snapshot record when the append was written.
|
|
279
|
+
* - No ordering is enforced. `version` orders a stream; timestamps may
|
|
280
|
+
* repeat, run backwards or lie in the future, as imported history can.
|
|
281
|
+
*/
|
|
282
|
+
readonly eventTimestamps?: ReadonlyArray<DateTime.DateTime | undefined>;
|
|
150
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Options accepted by {@link EventStream.append}: {@link AppendOptionsBase}
|
|
286
|
+
* plus the exclusive `metadata` / `eventMetadata` pair
|
|
287
|
+
* ({@link AppendMetadataOptions}).
|
|
288
|
+
*
|
|
289
|
+
* A type alias rather than an interface, since it is an intersection with a
|
|
290
|
+
* union: an interface cannot extend it.
|
|
291
|
+
*/
|
|
292
|
+
export type AppendOptions<TMetadata, TState = never> = AppendOptionsBase<TState> & AppendMetadataOptions<TMetadata>;
|
|
151
293
|
/** Error channel of {@link EventStream.append}. */
|
|
152
294
|
export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
295
|
+
/**
|
|
296
|
+
* Options accepted by {@link EventStream.read}, {@link EventStream.readFrom} and
|
|
297
|
+
* {@link EventStream.currentVersion}.
|
|
298
|
+
*
|
|
299
|
+
* `consistentRead: true` sets `ConsistentRead` on every `Query` page, so the
|
|
300
|
+
* read observes every append acknowledged before it started (read-your-writes).
|
|
301
|
+
* Default `false`: eventually consistent, at half the read cost.
|
|
302
|
+
*
|
|
303
|
+
* {@link commandHandler} loads state with strongly consistent reads by default —
|
|
304
|
+
* see {@link CommandHandlerOptions.consistentRead}.
|
|
305
|
+
*/
|
|
306
|
+
export interface ReadOptions {
|
|
307
|
+
readonly consistentRead?: boolean | undefined;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Options accepted by {@link EventStream.readLatest}: {@link ReadOptions} plus
|
|
311
|
+
* `verifySnapshot`.
|
|
312
|
+
*
|
|
313
|
+
* - `verifySnapshot` — default `true`: one `Query` reads the snapshot and the
|
|
314
|
+
* events after it, so the result is verified against the stream head.
|
|
315
|
+
* `false` reads **only the snapshot item**, with one `GetItem` (consistent
|
|
316
|
+
* per `consistentRead`), and returns
|
|
317
|
+
* `{ snapshot, events: [], version: snapshot.asOfVersion }` **without
|
|
318
|
+
* checking that no event follows it** — `version` is then the snapshot's
|
|
319
|
+
* version, not a verified head. When there is no snapshot item, it falls
|
|
320
|
+
* back to the verified read and returns that result.
|
|
321
|
+
*
|
|
322
|
+
* Why: a `Query`'s read capacity counts every item it reads, and the
|
|
323
|
+
* verified read's first page is the snapshot **plus** the newest event,
|
|
324
|
+
* even when the snapshot is current; a `GetItem` reads the snapshot alone.
|
|
325
|
+
* With large events that roughly halves the read capacity of a load.
|
|
326
|
+
*
|
|
327
|
+
* Only a stream whose snapshot config is `mode: "inline"` **without**
|
|
328
|
+
* `every` (the only mode that writes the snapshot in every append, so it is
|
|
329
|
+
* normally at the head) accepts `false`; any other snapshot config dies with
|
|
330
|
+
* `[EDD-9068]`. On a stream without a `snapshot` config the option is
|
|
331
|
+
* ignored. {@link commandHandler} exposes the same trade-off, with
|
|
332
|
+
* automatic fallbacks, as {@link CommandHandlerOptions.verifySnapshot}.
|
|
333
|
+
*/
|
|
334
|
+
export interface ReadLatestOptions extends ReadOptions {
|
|
335
|
+
readonly verifySnapshot?: boolean | undefined;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A stream's latest state as {@link EventStream.readLatest} loads it.
|
|
339
|
+
*
|
|
340
|
+
* - `snapshot` — the stream's snapshot, if one exists (always `None` on a
|
|
341
|
+
* stream without a `snapshot` config).
|
|
342
|
+
* - `events` — the events after the snapshot (every event when there is
|
|
343
|
+
* none), ascending by version. Fold them onto the snapshot's state.
|
|
344
|
+
* - `version` — the stream head: the newest event's version, the snapshot's
|
|
345
|
+
* `asOfVersion` when no event follows it, `0` for an empty stream. With
|
|
346
|
+
* `verifySnapshot: false` (see {@link ReadLatestOptions}) and a snapshot
|
|
347
|
+
* present, it is the snapshot's `asOfVersion` and `events` is empty —
|
|
348
|
+
* **unverified**: events appended without a snapshot (a raw `append`, data
|
|
349
|
+
* written before `mode: "inline"`) may follow it.
|
|
350
|
+
*/
|
|
351
|
+
export interface LatestState<TState, TEvent, M = Record<string, unknown> | undefined> {
|
|
352
|
+
readonly snapshot: Option.Option<Snapshot<TState>>;
|
|
353
|
+
readonly events: ReadonlyArray<StreamEvent<TEvent, M>>;
|
|
354
|
+
readonly version: number;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Derives an event's key in a stream index from the domain event (before it is
|
|
358
|
+
* encoded) and the version it is appended at.
|
|
359
|
+
*
|
|
360
|
+
* - A string is the event's sort key in the index. It is stored **raw**: no
|
|
361
|
+
* casing and no prefixing is applied, so compose it to sort the way you
|
|
362
|
+
* query (zero-pad numbers, for example).
|
|
363
|
+
* - `undefined` leaves the event out of the index (a sparse index).
|
|
364
|
+
* - An empty string (or any other value) fails the append with
|
|
365
|
+
* `ValidationError` before anything is written — DynamoDB rejects empty key
|
|
366
|
+
* values — as does a `key` that throws. A key over DynamoDB's 1024-byte
|
|
367
|
+
* sort-key limit is sent and rejected by DynamoDB; the append then fails
|
|
368
|
+
* with `ValidationError` naming the index and the event (#144), and nothing
|
|
369
|
+
* is written.
|
|
370
|
+
*
|
|
371
|
+
* The key is derived only when an event is appended. Events are never
|
|
372
|
+
* rewritten, so changing `key` later leaves events already stored under their
|
|
373
|
+
* old keys (or out of the index), and declaring an index on a stream that
|
|
374
|
+
* already holds events indexes only the events appended from then on. The
|
|
375
|
+
* library does not backfill: re-deriving and writing the index attributes on
|
|
376
|
+
* existing events is the application's decision (see `indexes` on
|
|
377
|
+
* {@link makeStream}).
|
|
378
|
+
*/
|
|
379
|
+
export type StreamIndexKey<TEvent> = (event: TEvent, version: number) => string | undefined;
|
|
380
|
+
/**
|
|
381
|
+
* A stream index on a **local** secondary index (the default `type`). An LSI
|
|
382
|
+
* shares the table's partition key (`pk`), so its items are exactly the
|
|
383
|
+
* stream's indexed events, readable with strongly consistent reads.
|
|
384
|
+
*
|
|
385
|
+
* - `index` — the physical LSI name.
|
|
386
|
+
* - `sk` — the attribute carrying the derived key (the LSI's sort key).
|
|
387
|
+
* - `key` — see {@link StreamIndexKey}.
|
|
388
|
+
*
|
|
389
|
+
* An LSI must be created with the table. A table with any LSI caps the item
|
|
390
|
+
* collection of **every** partition key value in it at 10 GB — not only the
|
|
391
|
+
* indexed stream's partitions, but every stream's (indexed or not) and every
|
|
392
|
+
* entity partition sharing the table; a write that would exceed it fails. What
|
|
393
|
+
* the index copies from each event is its `projection` — see
|
|
394
|
+
* {@link StreamIndexProjection}.
|
|
395
|
+
*/
|
|
396
|
+
export interface LocalStreamIndexConfig<TEvent> {
|
|
397
|
+
readonly type?: "lsi" | undefined;
|
|
398
|
+
readonly index: string;
|
|
399
|
+
readonly sk: string;
|
|
400
|
+
/** An LSI uses the table partition key; supplying `pk` is refused (`[EDD-9063]`). */
|
|
401
|
+
readonly pk?: undefined;
|
|
402
|
+
readonly key: StreamIndexKey<TEvent>;
|
|
403
|
+
/**
|
|
404
|
+
* What the index copies from each event — see {@link StreamIndexProjection}.
|
|
405
|
+
* Default `"all"`. Must match the deployed index.
|
|
406
|
+
*/
|
|
407
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* A stream index on a **global** secondary index. `append` writes the stream's
|
|
411
|
+
* partition key value into the `pk` attribute of each indexed event, so the
|
|
412
|
+
* index is scoped to the same stream: the eventually consistent equivalent of
|
|
413
|
+
* an LSI, without the LSI's 10 GB item-collection cap. DynamoDB rejects a
|
|
414
|
+
* strongly consistent read of it, diagnosed as a `ValidationError` naming the
|
|
415
|
+
* index.
|
|
416
|
+
*
|
|
417
|
+
* Unlike an LSI, the GSI itself can be added to an existing table
|
|
418
|
+
* (`UpdateTable`), but that does not index the stream's existing events: the
|
|
419
|
+
* index attributes are written only by `append`, so only events appended after
|
|
420
|
+
* the index is declared are in it — see {@link StreamIndexKey}.
|
|
421
|
+
*
|
|
422
|
+
* - `index` — the physical GSI name.
|
|
423
|
+
* - `pk` — the attribute carrying the stream's partition key (the GSI's HASH key).
|
|
424
|
+
* - `sk` — the attribute carrying the derived key (the GSI's RANGE key).
|
|
425
|
+
* - `key` — see {@link StreamIndexKey}.
|
|
426
|
+
* - `projection` — see {@link StreamIndexProjection}.
|
|
427
|
+
*/
|
|
428
|
+
export interface GlobalStreamIndexConfig<TEvent> {
|
|
429
|
+
readonly type: "gsi";
|
|
430
|
+
readonly index: string;
|
|
431
|
+
readonly pk: string;
|
|
432
|
+
readonly sk: string;
|
|
433
|
+
readonly key: StreamIndexKey<TEvent>;
|
|
434
|
+
/**
|
|
435
|
+
* What the index copies from each event — see {@link StreamIndexProjection}.
|
|
436
|
+
* Default `"all"`. Must match the deployed index.
|
|
437
|
+
*/
|
|
438
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* One entry of `makeStream({ indexes })`: a sub-stream of the stream's events,
|
|
442
|
+
* ordered by a key derived from each event. See {@link LocalStreamIndexConfig}
|
|
443
|
+
* and {@link GlobalStreamIndexConfig}.
|
|
444
|
+
*/
|
|
445
|
+
export type StreamIndexConfig<TEvent> = LocalStreamIndexConfig<TEvent> | GlobalStreamIndexConfig<TEvent>;
|
|
446
|
+
/**
|
|
447
|
+
* What a stream index copies from each event (`projection` on an entry of
|
|
448
|
+
* `makeStream({ indexes })`). {@link indexDefinitions} emits the matching
|
|
449
|
+
* `Projection`, and the reads choose their path from it.
|
|
450
|
+
*
|
|
451
|
+
* | Value | Emitted `Projection` | Read |
|
|
452
|
+
* |---|---|---|
|
|
453
|
+
* | `"all"` (default) | `ALL` | one query |
|
|
454
|
+
* | `"withoutMetadata"` | `INCLUDE` of every event attribute except `metadata` | one query; `metadata` is `undefined` (and typed so) |
|
|
455
|
+
* | `"keys"` | `INCLUDE ["__edd_e__"]` — **not** `KEYS_ONLY` | LSI: one query with `Select: ALL_ATTRIBUTES` (DynamoDB fetches each event from the table). GSI: the query, then `BatchGetItem` for the events of each page, in index order |
|
|
456
|
+
*
|
|
457
|
+
* `__edd_e__` is always projected because every read filters on it, and
|
|
458
|
+
* DynamoDB refuses a filter on an attribute an index does not project.
|
|
459
|
+
*
|
|
460
|
+
* `metadata` is often an event's largest attribute; `"withoutMetadata"` keeps
|
|
461
|
+
* it out of the index, and `"keys"` keeps the index entries minimal at the cost
|
|
462
|
+
* of a table read per event read (rounded up to 4 KB each).
|
|
463
|
+
*
|
|
464
|
+
* **The declared projection must match the deployed index.** The library does
|
|
465
|
+
* not migrate: an LSI's projection is fixed when the table is created, and a
|
|
466
|
+
* GSI's cannot be updated (it must be deleted and created again). Tables
|
|
467
|
+
* deployed with CDK or Terraform copy `Projection` from
|
|
468
|
+
* {@link indexDefinitions}' output. An index deployed narrower than declared
|
|
469
|
+
* fails reads: DynamoDB refuses the `__edd_e__` filter ("does not project one
|
|
470
|
+
* or more filter attributes"), or rows lack event attributes, which the read
|
|
471
|
+
* reports as a `ValidationError` (`operation: "EventStore.index.decode"`)
|
|
472
|
+
* naming the index.
|
|
473
|
+
*/
|
|
474
|
+
export type StreamIndexProjection = "all" | "withoutMetadata" | "keys";
|
|
475
|
+
/**
|
|
476
|
+
* The runtime settings of one stream index, as exposed on
|
|
477
|
+
* {@link EventStream.indexes}. `pk` is the index's partition key attribute:
|
|
478
|
+
* the table's `pk` for an LSI, the configured attribute for a GSI.
|
|
479
|
+
* `projection` is resolved: `"all"` when the index declares none.
|
|
480
|
+
*/
|
|
481
|
+
export interface StreamIndexSettings {
|
|
482
|
+
readonly type: "lsi" | "gsi";
|
|
483
|
+
readonly index: string;
|
|
484
|
+
readonly pk: string;
|
|
485
|
+
readonly sk: string;
|
|
486
|
+
readonly projection: StreamIndexProjection;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The `metadata` type of an event read through a stream index of projection
|
|
490
|
+
* `P`: `undefined` for a `"withoutMetadata"` index, which does not hold it,
|
|
491
|
+
* the stream's {@link StreamMetadata} otherwise.
|
|
492
|
+
*/
|
|
493
|
+
export type IndexEventMetadata<TMetadata, P extends StreamIndexProjection> = [P] extends [
|
|
494
|
+
"withoutMetadata"
|
|
495
|
+
] ? undefined : StreamMetadata<TMetadata>;
|
|
496
|
+
/** @internal The `projection` an index config declares (`"all"` when none). */
|
|
497
|
+
type ProjectionOf<C> = C extends {
|
|
498
|
+
readonly projection: infer P extends StreamIndexProjection;
|
|
499
|
+
} ? P : "all";
|
|
500
|
+
/**
|
|
501
|
+
* The projection of each index of a `makeStream({ indexes })` config, by name.
|
|
502
|
+
* `"all"` for an index that declares none.
|
|
503
|
+
*/
|
|
504
|
+
export type IndexProjectionsOf<TIndexes> = {
|
|
505
|
+
readonly [K in keyof TIndexes & string]: ProjectionOf<TIndexes[K]>;
|
|
506
|
+
};
|
|
507
|
+
/** @internal The members a stream index entry may declare. */
|
|
508
|
+
type StreamIndexConfigKey = keyof LocalStreamIndexConfig<unknown> | keyof GlobalStreamIndexConfig<unknown>;
|
|
509
|
+
/**
|
|
510
|
+
* The `indexes` parameter of {@link makeStream}: each entry is a
|
|
511
|
+
* {@link StreamIndexConfig}, and a member it does not declare (a misspelled
|
|
512
|
+
* `projection`, say) is typed `never`, so it is a compile error. Inferring
|
|
513
|
+
* `TIndexes` from the entries turns off TypeScript's own excess-property check,
|
|
514
|
+
* which would otherwise catch it.
|
|
515
|
+
*/
|
|
516
|
+
export type StreamIndexesConfig<TIndexes, TEvent> = {
|
|
517
|
+
readonly [K in keyof TIndexes]: StreamIndexConfig<TEvent> & {
|
|
518
|
+
readonly [P in Exclude<keyof TIndexes[K], StreamIndexConfigKey>]: never;
|
|
519
|
+
};
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* Options accepted by {@link EventStream.readIndex}.
|
|
523
|
+
*
|
|
524
|
+
* - `beginsWith` / `between` — a sort-key condition on the derived key (at most
|
|
525
|
+
* one of them, string bounds). Omitted, the whole sub-stream is read.
|
|
526
|
+
* DynamoDB rejects an empty bound, and a `between` whose lower bound sorts
|
|
527
|
+
* (by UTF-8 bytes) after its upper bound; the read then fails with a
|
|
528
|
+
* `ValidationError` naming the index and the bound, DynamoDB's error as its
|
|
529
|
+
* cause.
|
|
530
|
+
* - `reverse` — descending key order.
|
|
531
|
+
* - `limit` — return at most this many events (a positive integer).
|
|
532
|
+
* - `consistentRead` — strongly consistent read. DynamoDB rejects it on a
|
|
533
|
+
* GSI-backed index, which it reads only eventually consistently: the read
|
|
534
|
+
* fails with a `ValidationError` naming the index.
|
|
535
|
+
*/
|
|
536
|
+
export type ReadIndexOptions = {
|
|
537
|
+
readonly reverse?: boolean | undefined;
|
|
538
|
+
readonly limit?: number | undefined;
|
|
539
|
+
readonly consistentRead?: boolean | undefined;
|
|
540
|
+
} & ({
|
|
541
|
+
readonly beginsWith?: string | undefined;
|
|
542
|
+
readonly between?: undefined;
|
|
543
|
+
} | {
|
|
544
|
+
readonly between?: readonly [string, string] | undefined;
|
|
545
|
+
readonly beginsWith?: undefined;
|
|
546
|
+
});
|
|
153
547
|
type StreamIdInput<T extends ReadonlyArray<string>> = {
|
|
154
548
|
readonly [K in T[number]]: string;
|
|
155
549
|
};
|
|
@@ -160,14 +554,34 @@ export type EventStreamTypeId = typeof EventStreamTypeId;
|
|
|
160
554
|
*
|
|
161
555
|
* Created via {@link makeStream}. Operations are called directly on the stream:
|
|
162
556
|
* `MatchEvents.append(...)`, `MatchEvents.read(...)`, `MatchEvents.query.events(...)`.
|
|
557
|
+
*
|
|
558
|
+
* `TIndexName` is the union of the stream's index names (`makeStream({ indexes })`),
|
|
559
|
+
* `never` for a stream without indexes, so {@link EventStream.readIndex} and
|
|
560
|
+
* `query.index` only accept a declared name. `TIndexProjections` is each
|
|
561
|
+
* index's {@link StreamIndexProjection} by name, so an event read through a
|
|
562
|
+
* `"withoutMetadata"` index has `metadata: undefined`.
|
|
163
563
|
*/
|
|
164
|
-
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never
|
|
564
|
+
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
|
|
565
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
566
|
+
} = {
|
|
567
|
+
readonly [K in TIndexName]: "all";
|
|
568
|
+
}> extends Pipeable.Pipeable {
|
|
165
569
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
166
570
|
readonly streamName: string;
|
|
167
571
|
readonly eventSchema: Schema.Top;
|
|
572
|
+
/**
|
|
573
|
+
* The stream's indexes (`makeStream({ indexes })`) by name — their physical
|
|
574
|
+
* index and key attributes. Empty for a stream without indexes.
|
|
575
|
+
* {@link indexDefinitions} derives `CreateTable` fragments from it.
|
|
576
|
+
*/
|
|
577
|
+
readonly indexes: {
|
|
578
|
+
readonly [K in TIndexName]: StreamIndexSettings;
|
|
579
|
+
};
|
|
168
580
|
/**
|
|
169
581
|
* Present iff the stream was created with a `snapshot` config. Its presence
|
|
170
|
-
* is what switches {@link commandHandler} onto the snapshot-aware read path
|
|
582
|
+
* is what switches {@link commandHandler} onto the snapshot-aware read path
|
|
583
|
+
* ({@link EventStream.readLatest}); its `mode` decides how the handler writes
|
|
584
|
+
* snapshots.
|
|
171
585
|
*/
|
|
172
586
|
readonly snapshotConfig: SnapshotSettings | undefined;
|
|
173
587
|
/**
|
|
@@ -196,12 +610,90 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
196
610
|
* hide state-schema evolution bugs.
|
|
197
611
|
*/
|
|
198
612
|
readSnapshot(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<Option.Option<Snapshot<TState>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Append `events` after `expectedVersion`, atomically, in one transaction —
|
|
615
|
+
* see {@link AppendOptions}.
|
|
616
|
+
* A method, like `writeSnapshot`, so a snapshot-less stream (whose
|
|
617
|
+
* `options.snapshot` is `never`) stays assignable to other streams.
|
|
618
|
+
*/
|
|
619
|
+
append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, DynamoClient | TableConfig>;
|
|
620
|
+
/**
|
|
621
|
+
* Read every event of the stream, ascending by version. Pass
|
|
622
|
+
* `{ consistentRead: true }` for read-your-writes — see {@link ReadOptions}.
|
|
623
|
+
*/
|
|
624
|
+
read(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
625
|
+
/**
|
|
626
|
+
* Read the events after `afterVersion` (exclusive), ascending. Pass
|
|
627
|
+
* `{ consistentRead: true }` for read-your-writes — see {@link ReadOptions}.
|
|
628
|
+
*/
|
|
629
|
+
readFrom(streamId: StreamIdInput<TStreamIdFields>, afterVersion: number, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
630
|
+
/**
|
|
631
|
+
* The version of the stream's newest event (`0` for an empty stream), in one
|
|
632
|
+
* request. Pass `{ consistentRead: true }` for read-your-writes — see
|
|
633
|
+
* {@link ReadOptions}.
|
|
634
|
+
*/
|
|
635
|
+
currentVersion(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<number, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
636
|
+
/**
|
|
637
|
+
* The stream's snapshot (if any), the events after it, and its head — in
|
|
638
|
+
* **one request** in steady state. This is how {@link commandHandler} loads
|
|
639
|
+
* state on a snapshot-configured stream.
|
|
640
|
+
*
|
|
641
|
+
* Issues a single reverse `Query` over the sort-key range from the first
|
|
642
|
+
* event to the snapshot (the snapshot sorts after every event, and the
|
|
643
|
+
* idempotency sentinels before them, so the range holds exactly the events
|
|
644
|
+
* and the snapshot), with a first page of `(every ?? 1) + 1` items. Further
|
|
645
|
+
* pages are read only while the snapshot's `asOfVersion` has not been reached
|
|
646
|
+
* (a snapshot lagging by more than `every` events) — each sized to the events
|
|
647
|
+
* still missing, so a lagging snapshot costs one more request — or to the
|
|
648
|
+
* start of the stream when there is no snapshot. Pass
|
|
649
|
+
* `{ consistentRead: true }` for read-your-writes; it applies to the snapshot
|
|
650
|
+
* and the events alike.
|
|
651
|
+
*
|
|
652
|
+
* A snapshot that fails to decode through the state schema fails with
|
|
653
|
+
* `ValidationError`, as {@link EventStream.readSnapshot} does. On a stream
|
|
654
|
+
* without a `snapshot` config it is {@link EventStream.read} plus the head.
|
|
655
|
+
*
|
|
656
|
+
* `{ verifySnapshot: false }` reads only the snapshot item, with one
|
|
657
|
+
* `GetItem`, and returns it with no events and its `asOfVersion` as an
|
|
658
|
+
* **unverified** `version`; with no snapshot item it falls back to the
|
|
659
|
+
* verified read. Accepted only on a `mode: "inline"` stream without `every`
|
|
660
|
+
* (otherwise a defect, `[EDD-9068]`), ignored on a stream without a
|
|
661
|
+
* `snapshot` config — see {@link ReadLatestOptions}.
|
|
662
|
+
*/
|
|
663
|
+
readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
664
|
+
/**
|
|
665
|
+
* Read the stream's events in the order of one of its indexes (#140) — the
|
|
666
|
+
* events whose index `key` returned a string, ordered by that key. A
|
|
667
|
+
* convenience over `query.index`; pass `beginsWith` or `between` to narrow
|
|
668
|
+
* the key range, `reverse`, `limit` and `consistentRead` (which DynamoDB
|
|
669
|
+
* rejects on a GSI; the error names the index) — see {@link ReadIndexOptions}.
|
|
670
|
+
*
|
|
671
|
+
* How the events are read follows the index's {@link StreamIndexProjection}:
|
|
672
|
+
* one query for `"all"` and `"withoutMetadata"` (whose events have
|
|
673
|
+
* `metadata: undefined`, as the type says); for `"keys"`, one query with
|
|
674
|
+
* `Select: ALL_ATTRIBUTES` on an LSI, or the query followed by `BatchGetItem`
|
|
675
|
+
* (unprocessed keys retried, index order kept) on a GSI.
|
|
676
|
+
*/
|
|
677
|
+
readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
203
678
|
readonly query: {
|
|
204
679
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
680
|
+
/**
|
|
681
|
+
* A query of one of the stream's indexes (#140), scoped to `streamId`:
|
|
682
|
+
* decoded events in index-key order. It composes with the `Query`
|
|
683
|
+
* combinators — `Query.where` (a condition on the derived key),
|
|
684
|
+
* `Query.reverse`, `Query.limit`, `Query.consistentRead` (DynamoDB rejects
|
|
685
|
+
* it on a GSI-backed index: a `ValidationError` naming the index) — and terminals
|
|
686
|
+
* (`Query.collect`, `Query.execute`, `Query.paginate`). The
|
|
687
|
+
* `__edd_e__ = <stream>.event` filter still applies.
|
|
688
|
+
*
|
|
689
|
+
* It reads as {@link EventStream.readIndex} does for the index's
|
|
690
|
+
* projection: a `"keys"` GSI fetches each page's events with
|
|
691
|
+
* `BatchGetItem` (`Query.select` moves onto that fetch); `Query.count`
|
|
692
|
+
* counts index entries and fetches nothing. On a `"keys"` or
|
|
693
|
+
* `"withoutMetadata"` index, a `Query.filter` on an attribute the index
|
|
694
|
+
* does not project is refused by DynamoDB — use `Query.filterBy`.
|
|
695
|
+
*/
|
|
696
|
+
index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
|
|
205
697
|
};
|
|
206
698
|
}
|
|
207
699
|
/**
|
|
@@ -244,6 +736,10 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
244
736
|
* })
|
|
245
737
|
* ```
|
|
246
738
|
*
|
|
739
|
+
* `snapshot.mode: "inline"` writes the snapshot in the append transaction
|
|
740
|
+
* itself, so it is current after every command and state loads in one
|
|
741
|
+
* request — see {@link SnapshotMode}.
|
|
742
|
+
*
|
|
247
743
|
* `casing` overrides the key casing for this stream, as an index's or a vector
|
|
248
744
|
* index's `casing` does: the stream name, stream-id values and command ids in
|
|
249
745
|
* its keys all take it. When omitted, the stream name is lower-cased and the
|
|
@@ -254,11 +750,65 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
254
750
|
* (`<stream>.event` etc.) are always lower-cased. In the next major, omitting
|
|
255
751
|
* `casing` will mean the schema's casing, as it does for indexes.
|
|
256
752
|
*
|
|
753
|
+
* `indexes` declares sub-streams ordered by a key derived from each event
|
|
754
|
+
* (#140), on a local (default) or global secondary index. `append` writes each
|
|
755
|
+
* event's derived key (and, for a GSI, the stream's partition key) on the
|
|
756
|
+
* event item; snapshots and idempotency sentinels never carry them, so they
|
|
757
|
+
* are never in an index. Read with {@link EventStream.readIndex} or
|
|
758
|
+
* `query.index`, and create the indexes with {@link indexDefinitions}. Each
|
|
759
|
+
* index's `projection` (default `"all"`) sets what it copies from each event
|
|
760
|
+
* and how it is read — see {@link StreamIndexProjection}.
|
|
761
|
+
*
|
|
762
|
+
* Index attributes are written only at append time and events are never
|
|
763
|
+
* rewritten. Declaring an index on a stream that already holds events, or
|
|
764
|
+
* changing an index's `key`, therefore leaves the earlier events out of the
|
|
765
|
+
* index (or under their old keys), and `readIndex` returns only what was
|
|
766
|
+
* indexed — without an error. The library does not backfill; whether and how
|
|
767
|
+
* to re-index existing events (for example, an `UpdateItem` per event setting
|
|
768
|
+
* the derived attributes) is the application's decision.
|
|
769
|
+
*
|
|
770
|
+
* @example
|
|
771
|
+
* ```typescript
|
|
772
|
+
* const Entries = EventStore.makeStream({
|
|
773
|
+
* table: EventsTable,
|
|
774
|
+
* streamName: "Ledger",
|
|
775
|
+
* events: [EntryRecorded, EntryVoided],
|
|
776
|
+
* streamId: { composite: ["ledgerId"] },
|
|
777
|
+
* indexes: {
|
|
778
|
+
* byEntry: {
|
|
779
|
+
* index: "lsi1",
|
|
780
|
+
* sk: "lsi1sk",
|
|
781
|
+
* key: (event, version) =>
|
|
782
|
+
* event._tag === "EntryRecorded" ? `ENTRY#${event.section}#${pad(version)}` : undefined,
|
|
783
|
+
* },
|
|
784
|
+
* // Every event, by the day it was recorded (both event types carry `day`).
|
|
785
|
+
* byDay: { type: "gsi", index: "gsi1", pk: "gsi1pk", sk: "gsi1sk", key: (e) => e.day },
|
|
786
|
+
* },
|
|
787
|
+
* })
|
|
788
|
+
*
|
|
789
|
+
* const sectionThree = yield* Entries.readIndex("byEntry", { ledgerId }, { beginsWith: "ENTRY#3#" })
|
|
790
|
+
* ```
|
|
791
|
+
*
|
|
257
792
|
* @throws `[EDD-9027]` when `snapshot.every` is not a positive integer.
|
|
793
|
+
* @throws `[EDD-9062]` when `snapshot.mode` is neither `"after-append"` nor
|
|
794
|
+
* `"inline"`.
|
|
795
|
+
* @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
|
|
796
|
+
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, an empty `index` / `sk`
|
|
797
|
+
* / `pk`, a `key` that is not a function, a `projection` other than
|
|
798
|
+
* `"all"` / `"withoutMetadata"` / `"keys"`, or a member an index entry does
|
|
799
|
+
* not declare (such as a misspelled `projection`, also a type error).
|
|
800
|
+
* @throws `[EDD-9064]` when an index attribute is one the stream writes itself
|
|
801
|
+
* (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
|
|
802
|
+
* `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
|
|
803
|
+
* default TTL attribute `_ttl`). A custom `TableConfig.ttlAttributeName`
|
|
804
|
+
* is only known at runtime: an index attribute equal to it fails each append
|
|
805
|
+
* with `ValidationError`.
|
|
806
|
+
* @throws `[EDD-9065]` when two indexes share a physical index name or an
|
|
807
|
+
* attribute (or a GSI's `pk` and `sk` are the same attribute).
|
|
258
808
|
*/
|
|
259
809
|
export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top>, TTable extends Table, const TStreamName extends string, const TStreamId extends {
|
|
260
810
|
readonly composite: ReadonlyArray<string>;
|
|
261
|
-
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined>(config: {
|
|
811
|
+
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexes extends { readonly [K in keyof TIndexes]: StreamIndexConfig<Schema.Schema.Type<TEvents[number]>>; } = {}>(config: {
|
|
262
812
|
readonly table: TTable;
|
|
263
813
|
readonly streamName: TStreamName;
|
|
264
814
|
readonly events: TEvents;
|
|
@@ -266,7 +816,8 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
266
816
|
readonly metadata?: TMetadata;
|
|
267
817
|
readonly snapshot?: TSnapshot;
|
|
268
818
|
readonly casing?: DynamoSchema.Casing | undefined;
|
|
269
|
-
|
|
819
|
+
readonly indexes?: TIndexes & StreamIndexesConfig<TIndexes, Schema.Schema.Type<TEvents[number]>>;
|
|
820
|
+
}) => EventStream<Schema.Schema.Type<TEvents[number]>, TStreamId["composite"], TMetadata extends Schema.Top ? Schema.Schema.Type<TMetadata> : undefined, TSnapshot extends SnapshotConfig<infer TStateSchema> ? Schema.Schema.Type<TStateSchema> : never, keyof TIndexes & string, IndexProjectionsOf<TIndexes>>;
|
|
270
821
|
/**
|
|
271
822
|
* An EventStream whose operations have `DynamoClient` and `TableConfig` already
|
|
272
823
|
* resolved, so all methods return `Effect<A, E, never>`.
|
|
@@ -287,22 +838,44 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
287
838
|
* }) {}
|
|
288
839
|
* ```
|
|
289
840
|
*/
|
|
290
|
-
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never
|
|
841
|
+
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
|
|
842
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
843
|
+
} = {
|
|
844
|
+
readonly [K in TIndexName]: "all";
|
|
845
|
+
}> extends Pipeable.Pipeable {
|
|
291
846
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
292
847
|
readonly streamName: string;
|
|
293
848
|
readonly eventSchema: Schema.Top;
|
|
294
849
|
/** See {@link EventStream.snapshotConfig}. */
|
|
295
850
|
readonly snapshotConfig: SnapshotSettings | undefined;
|
|
851
|
+
/** See {@link EventStream.indexes}. */
|
|
852
|
+
readonly indexes: {
|
|
853
|
+
readonly [K in TIndexName]: StreamIndexSettings;
|
|
854
|
+
};
|
|
296
855
|
/** See {@link EventStream.writeSnapshot} — a method for the same variance reason. */
|
|
297
856
|
writeSnapshot(streamId: StreamIdInput<TStreamIdFields>, state: TState, asOfVersion: number): Effect.Effect<void, DynamoClientError | ValidationError, never>;
|
|
298
857
|
/** See {@link EventStream.readSnapshot}. */
|
|
299
858
|
readSnapshot(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<Option.Option<Snapshot<TState>>, DynamoClientError | ValidationError, never>;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
859
|
+
/** See {@link EventStream.append}. */
|
|
860
|
+
append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, never>;
|
|
861
|
+
/** See {@link EventStream.read}. */
|
|
862
|
+
read(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
|
|
863
|
+
/** See {@link EventStream.readFrom}. */
|
|
864
|
+
readFrom(streamId: StreamIdInput<TStreamIdFields>, afterVersion: number, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
|
|
865
|
+
/** See {@link EventStream.currentVersion}. */
|
|
866
|
+
currentVersion(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<number, DynamoClientError | ValidationError, never>;
|
|
867
|
+
/** See {@link EventStream.readLatest}. */
|
|
868
|
+
readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, never>;
|
|
869
|
+
/** See {@link EventStream.readIndex}. */
|
|
870
|
+
readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, never>;
|
|
871
|
+
/**
|
|
872
|
+
* The stream's query builders — unbound: run them with {@link provide}, or
|
|
873
|
+
* where `DynamoClient` and `TableConfig` are provided.
|
|
874
|
+
*/
|
|
304
875
|
readonly query: {
|
|
305
876
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
877
|
+
/** See `EventStream.query.index`. */
|
|
878
|
+
index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
|
|
306
879
|
};
|
|
307
880
|
/** Escape hatch: provide DynamoClient | TableConfig to an arbitrary effect. */
|
|
308
881
|
readonly provide: <A, E>(effect: Effect.Effect<A, E, DynamoClient | TableConfig>) => Effect.Effect<A, E, never>;
|
|
@@ -321,7 +894,81 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
|
|
|
321
894
|
* yield* stream.append({ matchId: "m-1" }, [event], 0) // R = never
|
|
322
895
|
* ```
|
|
323
896
|
*/
|
|
324
|
-
export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState>, never, DynamoClient | TableConfig>;
|
|
897
|
+
export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState, TIndexName extends string = never, TIndexProjections extends { readonly [K in TIndexName]: StreamIndexProjection; } = { readonly [K in TIndexName]: "all"; }>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, never, DynamoClient | TableConfig>;
|
|
898
|
+
/**
|
|
899
|
+
* `CreateTable` fragments for the stream indexes of one or more streams — see
|
|
900
|
+
* {@link indexDefinitions}. The lists are absent when empty, because
|
|
901
|
+
* DynamoDB rejects an empty index list.
|
|
902
|
+
*/
|
|
903
|
+
export interface StreamIndexDefinitions {
|
|
904
|
+
/** The index key attributes (all strings), excluding the table's `pk` / `sk`. */
|
|
905
|
+
readonly AttributeDefinitions: Array<AttributeDefinition>;
|
|
906
|
+
readonly LocalSecondaryIndexes?: Array<LocalSecondaryIndex> | undefined;
|
|
907
|
+
readonly GlobalSecondaryIndexes?: Array<GlobalSecondaryIndex> | undefined;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Derive the `CreateTable` fragments for the stream indexes (#140) of the given
|
|
911
|
+
* streams (`EventStream` or `BoundEventStream`) — `AttributeDefinitions`,
|
|
912
|
+
* `LocalSecondaryIndexes` and `GlobalSecondaryIndexes` — to merge into the
|
|
913
|
+
* caller's `CreateTable` input. Event tables are not derived by
|
|
914
|
+
* `Table.definition`, and the table's own `pk` / `sk` are not included: add
|
|
915
|
+
* them (and, on a provisioned table, each GSI's `ProvisionedThroughput`)
|
|
916
|
+
* yourself.
|
|
917
|
+
*
|
|
918
|
+
* Each index's `Projection` follows its {@link StreamIndexProjection}:
|
|
919
|
+
*
|
|
920
|
+
* | `projection` | `Projection` |
|
|
921
|
+
* |---|---|
|
|
922
|
+
* | `"all"` (default) | `{ ProjectionType: "ALL" }` |
|
|
923
|
+
* | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
|
|
924
|
+
* | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
|
|
925
|
+
*
|
|
926
|
+
* The deployed index must project what is declared — copy `Projection` from
|
|
927
|
+
* this output into CDK or Terraform definitions. Neither an LSI's nor a GSI's
|
|
928
|
+
* projection can be changed in place, and the library migrates nothing. A
|
|
929
|
+
* table projects at most 100 `NonKeyAttributes` across all its indexes;
|
|
930
|
+
* `"withoutMetadata"` spends 6 of them and `"keys"` 1 (DynamoDB refuses a
|
|
931
|
+
* table over the limit).
|
|
932
|
+
*
|
|
933
|
+
* An index shared by several streams with the same definition is emitted
|
|
934
|
+
* once. Lists are sorted by index name, attributes by attribute name.
|
|
935
|
+
*
|
|
936
|
+
* - An LSI can only be created with the table, and caps the item collection
|
|
937
|
+
* of every partition key value in the table at 10 GB — entity partitions
|
|
938
|
+
* and streams without indexes included.
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* ```typescript
|
|
942
|
+
* const fragments = EventStore.indexDefinitions(Entries, Orders)
|
|
943
|
+
* yield* client.createTable({
|
|
944
|
+
* TableName: "events",
|
|
945
|
+
* BillingMode: "PAY_PER_REQUEST",
|
|
946
|
+
* KeySchema: [
|
|
947
|
+
* { AttributeName: "pk", KeyType: "HASH" },
|
|
948
|
+
* { AttributeName: "sk", KeyType: "RANGE" },
|
|
949
|
+
* ],
|
|
950
|
+
* AttributeDefinitions: [
|
|
951
|
+
* { AttributeName: "pk", AttributeType: "S" },
|
|
952
|
+
* { AttributeName: "sk", AttributeType: "S" },
|
|
953
|
+
* ...fragments.AttributeDefinitions,
|
|
954
|
+
* ],
|
|
955
|
+
* LocalSecondaryIndexes: fragments.LocalSecondaryIndexes,
|
|
956
|
+
* GlobalSecondaryIndexes: fragments.GlobalSecondaryIndexes,
|
|
957
|
+
* })
|
|
958
|
+
* ```
|
|
959
|
+
*
|
|
960
|
+
* @throws `[EDD-9067]` when two streams define the same physical index
|
|
961
|
+
* differently (another type, other key attributes, or another projection).
|
|
962
|
+
*/
|
|
963
|
+
export declare const indexDefinitions: (...streams: ReadonlyArray<{
|
|
964
|
+
readonly streamName: string;
|
|
965
|
+
readonly indexes: {
|
|
966
|
+
readonly [name: string]: Omit<StreamIndexSettings, "projection"> & {
|
|
967
|
+
/** Read as `"all"` when absent (a hand-built settings object). */
|
|
968
|
+
readonly projection?: StreamIndexProjection | undefined;
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
}>) => StreamIndexDefinitions;
|
|
325
972
|
/**
|
|
326
973
|
* Handler-level configuration for {@link commandHandler}.
|
|
327
974
|
*
|
|
@@ -336,45 +983,350 @@ export interface CommandHandlerOptions {
|
|
|
336
983
|
* The retried unit is the entire read–decide–append cycle, so every attempt
|
|
337
984
|
* decides against freshly read state — a blind re-append of stale events is
|
|
338
985
|
* impossible by construction. Snapshot reads participate: a retried attempt
|
|
339
|
-
* re-reads the snapshot and its delta
|
|
986
|
+
* re-reads the snapshot and its delta (the snapshot alone with
|
|
987
|
+
* {@link verifySnapshot}`: false`), and a function-form `additionalItems` is
|
|
988
|
+
* re-evaluated against the new decision.
|
|
340
989
|
*
|
|
341
990
|
* A number `n` is shorthand for `Schedule.recurs(n)` (n retries *after* the
|
|
342
991
|
* initial attempt). Omit for the default: no retry.
|
|
343
992
|
*
|
|
344
|
-
* `DuplicateCommand` is deliberately NOT retried — it is terminal.
|
|
993
|
+
* `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
|
|
994
|
+
* any call that supplies {@link CommandOptionsBase.expectedVersion}: the caller asked
|
|
995
|
+
* for a conditional write, so its `VersionConflict` is always surfaced.
|
|
345
996
|
*/
|
|
346
997
|
readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
|
|
347
998
|
/** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
|
|
348
999
|
readonly idempotency?: {
|
|
349
1000
|
readonly ttl?: Duration.Duration | string;
|
|
350
1001
|
};
|
|
1002
|
+
/**
|
|
1003
|
+
* Load state with strongly consistent reads. Default `true`.
|
|
1004
|
+
*
|
|
1005
|
+
* An eventually consistent load can miss the newest events, so `decide` runs
|
|
1006
|
+
* against stale state and the append then fails with `VersionConflict` (or
|
|
1007
|
+
* burns a retry). A consistent load costs twice the read capacity of an
|
|
1008
|
+
* eventually consistent one. Set `false` to trade that for the occasional
|
|
1009
|
+
* conflict. On a snapshot-configured stream the snapshot and its delta are
|
|
1010
|
+
* one query ({@link EventStream.readLatest}), so the setting covers both.
|
|
1011
|
+
*/
|
|
1012
|
+
readonly consistentRead?: boolean | undefined;
|
|
1013
|
+
/**
|
|
1014
|
+
* Verify the snapshot against the stream head when loading state. Default
|
|
1015
|
+
* `true`.
|
|
1016
|
+
*
|
|
1017
|
+
* With `true`, a snapshot-configured stream loads with one `Query` that
|
|
1018
|
+
* reads the snapshot **and** the newest event (more on a lagging snapshot),
|
|
1019
|
+
* so the state `decide` sees is verified current. `false` loads with one
|
|
1020
|
+
* `GetItem` of the snapshot item only ({@link EventStream.readLatest} with
|
|
1021
|
+
* `verifySnapshot: false`). A `Query`'s read capacity counts every item it
|
|
1022
|
+
* reads, a `GetItem` only the snapshot, so with large events `false`
|
|
1023
|
+
* roughly halves the read capacity per command.
|
|
1024
|
+
*
|
|
1025
|
+
* Accepted only when the stream's snapshot config is `mode: "inline"`
|
|
1026
|
+
* **without** `every` — the only mode that writes the snapshot in every
|
|
1027
|
+
* append's transaction, so it is normally at the head. Any other snapshot
|
|
1028
|
+
* config throws `[EDD-9068]` when the handler is created (data-first or
|
|
1029
|
+
* data-last). On a stream without a `snapshot` config the option is
|
|
1030
|
+
* ignored: state is replayed and verified as always.
|
|
1031
|
+
*
|
|
1032
|
+
* The snapshot can still lag the head — events appended without one (a raw
|
|
1033
|
+
* `append`, data written before `mode: "inline"`). So no answer made on an
|
|
1034
|
+
* unverified snapshot is returned until the head confirms it: a successful
|
|
1035
|
+
* append confirms it by itself; anything else — a `decide` failure, a no-op
|
|
1036
|
+
* decision, an append `VersionConflict` — is checked with one more read,
|
|
1037
|
+
* and a stale load falls back to a verified one:
|
|
1038
|
+
*
|
|
1039
|
+
* - **No snapshot item** (a stream not yet snapshotted): the verified load
|
|
1040
|
+
* runs at once, and `decide` runs once.
|
|
1041
|
+
* - **With {@link CommandOptionsBase.expectedVersion}**: a snapshot whose
|
|
1042
|
+
* `asOfVersion` differs from it is re-read verified before the If-Match
|
|
1043
|
+
* check, so only a genuine mismatch is reported (its `actualVersion` is
|
|
1044
|
+
* the verified head). A snapshot at the expected version holds exactly the
|
|
1045
|
+
* state the caller saw, so `decide` runs on it; if that decision does not
|
|
1046
|
+
* append, one head `Query` above the If-Match checks the head (see
|
|
1047
|
+
* below). A head past the
|
|
1048
|
+
* If-Match discards the decision and answers as a verified load would:
|
|
1049
|
+
* `DuplicateCommand` for a committed redelivery, else `VersionConflict`
|
|
1050
|
+
* with the head as `actualVersion` — never re-decided or retried.
|
|
1051
|
+
* - **Without it**, a `decide` failure or a no-op is checked against the
|
|
1052
|
+
* head the same way and returned when the head is at the snapshot. An
|
|
1053
|
+
* append that fails with `VersionConflict` triggers one verified load:
|
|
1054
|
+
* a snapshot at the head means another writer won a genuine race, and
|
|
1055
|
+
* the `VersionConflict` goes to the {@link retry} policy as it always
|
|
1056
|
+
* does. Otherwise — the head past the snapshot, or past the version
|
|
1057
|
+
* decided on — the first decision was made on stale state and is
|
|
1058
|
+
* discarded: the handler loads verified (if it has not) and decides again
|
|
1059
|
+
* on that state, then appends — **`decide` (and a function-form
|
|
1060
|
+
* `additionalItems`, for a decision that appends) runs twice for that
|
|
1061
|
+
* call**. That is the optimistic-concurrency price of skipping the
|
|
1062
|
+
* verification read, the same one a `VersionConflict` retry pays: a
|
|
1063
|
+
* decision is only as current as the state it was made on, and a stale
|
|
1064
|
+
* one cannot be patched, only made again. Without the re-decision, a
|
|
1065
|
+
* lagging snapshot would fail or mis-answer every command on the stream,
|
|
1066
|
+
* since only a successful inline append rewrites it. It happens at most
|
|
1067
|
+
* once per attempt, and the second decision's outcome is final for the
|
|
1068
|
+
* attempt (its conflict follows the policy).
|
|
1069
|
+
*
|
|
1070
|
+
* The head check is a reverse `Limit: 1` `Query` bounded below by the
|
|
1071
|
+
* snapshot's version (or the If-Match), never a read of the newest event:
|
|
1072
|
+
* while the snapshot is current the range is empty, so it reads no items
|
|
1073
|
+
* and costs only DynamoDB's per-request minimum, however large the events
|
|
1074
|
+
* are. A decision that appends nothing therefore costs one extra round trip,
|
|
1075
|
+
* not extra item reads.
|
|
1076
|
+
*/
|
|
1077
|
+
readonly verifySnapshot?: boolean | undefined;
|
|
351
1078
|
}
|
|
352
|
-
/**
|
|
353
|
-
|
|
1079
|
+
/**
|
|
1080
|
+
* The outcome of one `decide` call, handed to a function-form
|
|
1081
|
+
* {@link CommandOptionsBase.additionalItems}, to `eventMetadata`
|
|
1082
|
+
* ({@link CommandMetadataOptions}) and to
|
|
1083
|
+
* {@link CommandOptionsBase.eventTimestamps}.
|
|
1084
|
+
*
|
|
1085
|
+
* - `events` — what `decide` returned (never empty: the function is not called
|
|
1086
|
+
* for a no-op decision).
|
|
1087
|
+
* - `state` — the `evolve` fold of `previous` with `events`: the state the
|
|
1088
|
+
* handler returns and snapshots.
|
|
1089
|
+
* - `previous` — the state `decide` was given.
|
|
1090
|
+
* - `version` — the stream version the events are appended after; the new
|
|
1091
|
+
* events take `version + 1 … version + events.length`.
|
|
1092
|
+
*
|
|
1093
|
+
* `evolve` may mutate state in place. When it does, `previous` and `state` are
|
|
1094
|
+
* the same reference and `previous` already reflects `events`; a projection
|
|
1095
|
+
* that needs a pristine `previous` requires an `evolve` that returns new state.
|
|
1096
|
+
*/
|
|
1097
|
+
export interface Decision<State, Event> {
|
|
1098
|
+
readonly events: ReadonlyArray<Event>;
|
|
1099
|
+
readonly state: State;
|
|
1100
|
+
readonly previous: State;
|
|
1101
|
+
readonly version: number;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* The forms {@link CommandOptionsBase.additionalItems} accepts: a static array of
|
|
1105
|
+
* transact ops, or a function of the {@link Decision} returning the array
|
|
1106
|
+
* directly (pure projection) or as an `Effect` (a projection that needs a read).
|
|
1107
|
+
* The effect's `E2` joins the handler's error channel and its `R2` the handler's
|
|
1108
|
+
* requirements.
|
|
1109
|
+
*/
|
|
1110
|
+
export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
|
|
1111
|
+
/**
|
|
1112
|
+
* The form `eventMetadata` ({@link CommandMetadataOptions}) accepts: a
|
|
1113
|
+
* function of the {@link Decision} returning one metadata entry per event
|
|
1114
|
+
* (`undefined` for an event without metadata), directly or as an `Effect`. The
|
|
1115
|
+
* effect's `E3` joins the handler's error channel and its `R3` the handler's
|
|
1116
|
+
* requirements.
|
|
1117
|
+
*
|
|
1118
|
+
* Function-only: the number of events is known only once `decide` has run.
|
|
1119
|
+
* The `Effect` form lets ids be minted through the `Crypto` service
|
|
1120
|
+
* (`crypto.randomULID`, or `randomUUIDv7`), which keeps them deterministic
|
|
1121
|
+
* under `TestClock` and a test `Crypto` layer with fixed bytes.
|
|
1122
|
+
*/
|
|
1123
|
+
export type EventMetadataInput<State, Event, TMetadata, E3 = never, R3 = never> = (decision: Decision<State, Event>) => ReadonlyArray<TMetadata | undefined> | Effect.Effect<ReadonlyArray<TMetadata | undefined>, E3, R3>;
|
|
1124
|
+
/**
|
|
1125
|
+
* The form {@link CommandOptionsBase.eventTimestamps} accepts: a function of
|
|
1126
|
+
* the {@link Decision} returning one timestamp per event (`undefined` keeps
|
|
1127
|
+
* the append's `Clock` time), directly or as an `Effect` — exactly as
|
|
1128
|
+
* {@link EventMetadataInput}. The effect's `E4` joins the handler's error
|
|
1129
|
+
* channel and its `R4` the handler's requirements.
|
|
1130
|
+
*
|
|
1131
|
+
* Function-only: the number of events is known only once `decide` has run.
|
|
1132
|
+
* The `Effect` form lets times come from a service (a source log, a
|
|
1133
|
+
* per-tenant clock) or from `Clock`, which keeps them deterministic under
|
|
1134
|
+
* `TestClock`.
|
|
1135
|
+
*
|
|
1136
|
+
* `E4` / `R4` are separate from `eventMetadata`'s `E3` / `R3`: with one shared
|
|
1137
|
+
* pair, an `eventMetadata` effect and an `eventTimestamps` effect with
|
|
1138
|
+
* different error types would not compile — TypeScript fixes the parameter
|
|
1139
|
+
* from one candidate and rejects the other instead of inferring a union.
|
|
1140
|
+
*/
|
|
1141
|
+
export type EventTimestampsInput<State, Event, E4 = never, R4 = never> = (decision: Decision<State, Event>) => ReadonlyArray<DateTime.DateTime | undefined> | Effect.Effect<ReadonlyArray<DateTime.DateTime | undefined>, E4, R4>;
|
|
1142
|
+
/**
|
|
1143
|
+
* The per-call metadata of a {@link commandHandler} handler: one value shared
|
|
1144
|
+
* by every event (`metadata`), or a function deriving one entry per event from
|
|
1145
|
+
* the decision (`eventMetadata`) — never both (see
|
|
1146
|
+
* {@link AppendMetadataOptions}).
|
|
1147
|
+
*/
|
|
1148
|
+
export type CommandMetadataOptions<TMetadata, State, Event, E3 = never, R3 = never> = {
|
|
1149
|
+
/**
|
|
1150
|
+
* Metadata written onto every event the decision appends (see
|
|
1151
|
+
* {@link AppendMetadataOptions}). Fixed before `decide` runs; to give
|
|
1152
|
+
* each event its own entry, use `eventMetadata`.
|
|
1153
|
+
*/
|
|
354
1154
|
readonly metadata?: TMetadata;
|
|
1155
|
+
readonly eventMetadata?: undefined;
|
|
1156
|
+
} | {
|
|
1157
|
+
readonly metadata?: undefined;
|
|
1158
|
+
/**
|
|
1159
|
+
* Per-event metadata derived from the {@link Decision}: the function
|
|
1160
|
+
* returns one entry per event, in order, and entry `i` is written onto
|
|
1161
|
+
* event `i` (see {@link AppendMetadataOptions}).
|
|
1162
|
+
*
|
|
1163
|
+
* - Runs after `decide` and after the new events are folded, receiving
|
|
1164
|
+
* the same `Decision` a function-form `additionalItems` does — and
|
|
1165
|
+
* **before** `additionalItems`, so an effectful pair runs in a
|
|
1166
|
+
* predictable order.
|
|
1167
|
+
* - Not called when `decide` returns no events.
|
|
1168
|
+
* - Re-run on every attempt: each `VersionConflict` retry, and the
|
|
1169
|
+
* re-decision a stale unverified snapshot triggers
|
|
1170
|
+
* ({@link CommandHandlerOptions.verifySnapshot}`: false`). Ids minted
|
|
1171
|
+
* per attempt are safe — a failed attempt writes nothing.
|
|
1172
|
+
* - A result whose length differs from the decision's events fails with
|
|
1173
|
+
* `ValidationError` (`operation: "EventStore.commandHandler.eventMetadata"`),
|
|
1174
|
+
* never retried. A failure of the returned `Effect` surfaces as is,
|
|
1175
|
+
* and is not retried either.
|
|
1176
|
+
*
|
|
1177
|
+
* Without `idempotency`, a redelivery after a lost response decides and
|
|
1178
|
+
* appends again, minting new ids. With it, the redelivery fails with
|
|
1179
|
+
* `DuplicateCommand`, so the first commit's ids are the only ones written:
|
|
1180
|
+
*
|
|
1181
|
+
* ```ts
|
|
1182
|
+
* // `crypto` is the `Crypto.Crypto` service; a ULID takes its time from the
|
|
1183
|
+
* // `Clock`, so `TestClock` plus a fixed-bytes `Crypto` make it deterministic
|
|
1184
|
+
* const handle = EventStore.commandHandler(decider, stream, {
|
|
1185
|
+
* idempotency: { ttl: Duration.days(1) },
|
|
1186
|
+
* })
|
|
1187
|
+
* yield* handle(streamId, command, {
|
|
1188
|
+
* commandId: deliveryId,
|
|
1189
|
+
* eventMetadata: ({ events }) =>
|
|
1190
|
+
* Effect.forEach(events, () =>
|
|
1191
|
+
* Effect.map(crypto.randomULID, (eventId) => ({ eventId })),
|
|
1192
|
+
* ),
|
|
1193
|
+
* })
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
readonly eventMetadata?: EventMetadataInput<State, Event, TMetadata, E3, R3>;
|
|
1197
|
+
};
|
|
1198
|
+
/**
|
|
1199
|
+
* The options of a {@link commandHandler} handler other than its metadata
|
|
1200
|
+
* (see {@link CommandMetadataOptions}).
|
|
1201
|
+
*/
|
|
1202
|
+
export interface CommandOptionsBase<State = unknown, Event = unknown, E2 = never, R2 = never, E4 = never, R4 = never> {
|
|
355
1203
|
/**
|
|
356
1204
|
* Identifier for this command delivery. Required when the handler was created
|
|
357
|
-
* with `idempotency`; a replayed id fails with `DuplicateCommand
|
|
1205
|
+
* with `idempotency`; a replayed id fails with `DuplicateCommand` — also when
|
|
1206
|
+
* the replay carries an {@link expectedVersion} the stream has since moved
|
|
1207
|
+
* past (see there).
|
|
1208
|
+
*
|
|
1209
|
+
* Otherwise the sentinel is consulted when the events are appended, after
|
|
1210
|
+
* `decide`, so a replay whose `decide` fails against the advanced state
|
|
1211
|
+
* reports that failure, and one whose `decide` returns no events succeeds as
|
|
1212
|
+
* a no-op. Only a redelivery that appends fails with `DuplicateCommand`.
|
|
358
1213
|
*/
|
|
359
1214
|
readonly commandId?: string;
|
|
360
|
-
/**
|
|
361
|
-
|
|
1215
|
+
/**
|
|
1216
|
+
* The stream version the caller last saw — an HTTP `If-Match`.
|
|
1217
|
+
*
|
|
1218
|
+
* After state is loaded and **before `decide` runs**, a stream at any other
|
|
1219
|
+
* version fails the call with `VersionConflict`, carrying the loaded version
|
|
1220
|
+
* as `actualVersion`; `decide` never sees state the caller did not. Otherwise
|
|
1221
|
+
* the append is conditioned on this version, so a writer that slips in
|
|
1222
|
+
* between the load and the append also fails it with `VersionConflict`
|
|
1223
|
+
* (without `actualVersion`). Neither is retried, whatever the handler's
|
|
1224
|
+
* `retry` policy says.
|
|
1225
|
+
*
|
|
1226
|
+
* With `idempotency`, a mismatch is first checked against the command's
|
|
1227
|
+
* sentinel (one strongly consistent `GetItem`, on the mismatch path only): a
|
|
1228
|
+
* redelivery of a command that already committed — its response lost, the
|
|
1229
|
+
* same `commandId` and If-Match sent again — fails with `DuplicateCommand`,
|
|
1230
|
+
* not `VersionConflict`, matching the precedence `append` applies.
|
|
1231
|
+
*
|
|
1232
|
+
* A no-op decision (`decide` returns `[]`) at the matching version succeeds
|
|
1233
|
+
* with the current state and version. Must be a non-negative integer — any
|
|
1234
|
+
* other value fails with `ValidationError` before anything is read.
|
|
1235
|
+
*/
|
|
1236
|
+
readonly expectedVersion?: number | undefined;
|
|
1237
|
+
/**
|
|
1238
|
+
* Caller-owned transact items committed atomically with the produced events
|
|
1239
|
+
* (see {@link AppendOptionsBase.additionalItems}).
|
|
1240
|
+
*
|
|
1241
|
+
* Either a static array, or a function of the {@link Decision} — an inline
|
|
1242
|
+
* projection — returning the array or an `Effect` of it. The function runs
|
|
1243
|
+
* after `decide` and after the new events are folded, is not called when
|
|
1244
|
+
* `decide` returns no events, and is re-run on every retry attempt. The items
|
|
1245
|
+
* it returns count towards `AppendTooLarge`, and the `indices` of an
|
|
1246
|
+
* `AdditionalItemConditionFailed` refer to the array it returned.
|
|
1247
|
+
*/
|
|
1248
|
+
readonly additionalItems?: AdditionalItemsInput<State, Event, E2, R2>;
|
|
1249
|
+
/**
|
|
1250
|
+
* Per-event timestamps derived from the {@link Decision}: the function
|
|
1251
|
+
* returns one entry per event, in order, directly or as an `Effect` (see
|
|
1252
|
+
* {@link EventTimestampsInput}), and entry `i` is written as event `i`'s
|
|
1253
|
+
* `timestamp` — `undefined` keeps the append's `Clock` time (see
|
|
1254
|
+
* {@link AppendOptionsBase.eventTimestamps}). Independent of `metadata` /
|
|
1255
|
+
* `eventMetadata`. The idempotency sentinel, its TTL and an inline snapshot
|
|
1256
|
+
* keep the append's `Clock` time; timestamps carry no ordering check.
|
|
1257
|
+
*
|
|
1258
|
+
* - Runs after `eventMetadata` and before `additionalItems`, each to
|
|
1259
|
+
* completion before the next, all receiving the same `Decision`; not
|
|
1260
|
+
* called when `decide` returns no events; re-run on every attempt — each
|
|
1261
|
+
* `VersionConflict` retry and the re-decision a stale unverified snapshot
|
|
1262
|
+
* triggers — as `eventMetadata` is.
|
|
1263
|
+
* - A failure of the returned `Effect` surfaces as is, is not retried, and
|
|
1264
|
+
* `additionalItems` is not run.
|
|
1265
|
+
* - A result — plain or produced by the `Effect` — of the wrong length, or
|
|
1266
|
+
* with an entry that is neither a valid `DateTime` (finite epoch) nor
|
|
1267
|
+
* `undefined`, fails with `ValidationError`
|
|
1268
|
+
* (`operation: "EventStore.commandHandler.eventTimestamps"`), never
|
|
1269
|
+
* retried. A value that is not a function is refused before anything is
|
|
1270
|
+
* read.
|
|
1271
|
+
*
|
|
1272
|
+
* ```ts
|
|
1273
|
+
* // plain: the times are in the events
|
|
1274
|
+
* eventTimestamps: ({ events }) => events.map((event) => event.occurredAt)
|
|
1275
|
+
*
|
|
1276
|
+
* // Effect: the times come from a service
|
|
1277
|
+
* eventTimestamps: ({ events }) =>
|
|
1278
|
+
* Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId))
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
readonly eventTimestamps?: EventTimestampsInput<State, Event, E4, R4>;
|
|
362
1282
|
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Per-call options accepted by a handler produced by {@link commandHandler}:
|
|
1285
|
+
* {@link CommandOptionsBase} plus the exclusive `metadata` / `eventMetadata`
|
|
1286
|
+
* pair ({@link CommandMetadataOptions}).
|
|
1287
|
+
*
|
|
1288
|
+
* `State` / `Event` type the {@link Decision} a function-form `additionalItems`,
|
|
1289
|
+
* `eventMetadata` or `eventTimestamps` receives; `E2` / `R2` are the error and
|
|
1290
|
+
* requirements of the `Effect` `additionalItems` may return, `E3` / `R3` those
|
|
1291
|
+
* of the `Effect` `eventMetadata` may return, `E4` / `R4` those of the
|
|
1292
|
+
* `Effect` `eventTimestamps` may return — separate, so effects with different
|
|
1293
|
+
* error types all join the handler's error channel. The handler infers all of
|
|
1294
|
+
* them, so they only need naming when this type is written out by hand.
|
|
1295
|
+
*/
|
|
1296
|
+
export type CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptionsBase<State, Event, E2, R2, E4, R4> & CommandMetadataOptions<TMetadata, State, Event, E3, R3>;
|
|
363
1297
|
/** Per-call options when the handler was created with `idempotency` — `commandId` is required. */
|
|
364
|
-
export
|
|
1298
|
+
export type IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptions<TMetadata, State, Event, E2, R2, E3, R3, E4, R4> & {
|
|
365
1299
|
readonly commandId: string;
|
|
366
|
-
}
|
|
1300
|
+
};
|
|
367
1301
|
/**
|
|
368
1302
|
* Options arity: configuring `idempotency` makes the handler's options parameter
|
|
369
1303
|
* required (and `commandId` within it non-optional), so a missing `commandId`
|
|
370
1304
|
* is a compile error rather than a silent downgrade to at-least-once.
|
|
371
1305
|
*/
|
|
372
|
-
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined> = TConfig extends {
|
|
1306
|
+
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2, E3, R3, E4, R4> = TConfig extends {
|
|
373
1307
|
readonly idempotency: object;
|
|
374
|
-
} ? [options: IdempotentCommandOptions<TMetadata>] : [options?: CommandOptions<TMetadata> | undefined];
|
|
1308
|
+
} ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4> | undefined];
|
|
375
1309
|
type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
376
|
-
|
|
377
|
-
|
|
1310
|
+
/**
|
|
1311
|
+
* A handler over an {@link EventStream}. Generic per call over the error (`E2`)
|
|
1312
|
+
* and requirements (`R2`) of a function-form `additionalItems` that returns an
|
|
1313
|
+
* `Effect`, those (`E3` / `R3`) of an `eventMetadata` that does, and those
|
|
1314
|
+
* (`E4` / `R4`) of an `eventTimestamps` that does; all are `never` otherwise.
|
|
1315
|
+
*/
|
|
1316
|
+
type CommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, DynamoClient | TableConfig | R2 | R3 | R4>;
|
|
1317
|
+
/**
|
|
1318
|
+
* A handler over a {@link BoundEventStream}: the stream's services are already
|
|
1319
|
+
* provided, so the `R2` / `R3` / `R4` of a function-form `additionalItems` /
|
|
1320
|
+
* `eventMetadata` / `eventTimestamps` are its only requirements.
|
|
1321
|
+
*/
|
|
1322
|
+
type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, R2 | R3 | R4>;
|
|
1323
|
+
/**
|
|
1324
|
+
* The handler the data-last {@link commandHandler} returns for stream `S`: a
|
|
1325
|
+
* {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
|
|
1326
|
+
* stream is also structurally an `EventStream`), a {@link CommandHandler}
|
|
1327
|
+
* otherwise.
|
|
1328
|
+
*/
|
|
1329
|
+
type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHandlerOptions | undefined> = S extends BoundEventStream<any, infer TStreamIdFields, infer TMetadata, any, any> ? BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig> : S extends EventStream<any, infer TStreamIdFields, infer TMetadata, any, any> ? CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig> : never;
|
|
378
1330
|
/**
|
|
379
1331
|
* Create a command handler that reads, decides, and appends atomically.
|
|
380
1332
|
*
|
|
@@ -400,12 +1352,69 @@ type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends Read
|
|
|
400
1352
|
* idempotency: { ttl: Duration.days(1) },
|
|
401
1353
|
* })
|
|
402
1354
|
* yield* handle({ matchId: "m-1" }, command, { commandId: "cmd-7f3a" })
|
|
1355
|
+
*
|
|
1356
|
+
* // Load state from the inline snapshot alone (mode "inline" without `every`)
|
|
1357
|
+
* const handle = EventStore.commandHandler(decider, stream, { verifySnapshot: false })
|
|
1358
|
+
*
|
|
1359
|
+
* // If-Match: fail with VersionConflict (never retried) unless the stream is at v7
|
|
1360
|
+
* yield* handle({ matchId: "m-1" }, command, { expectedVersion: 7 })
|
|
1361
|
+
*
|
|
1362
|
+
* // Inline projection, committed in the same transaction as the events
|
|
1363
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1364
|
+
* additionalItems: ({ state }) => [Scoreboard.put({ matchId: "m-1", runs: state.runs })],
|
|
1365
|
+
* })
|
|
1366
|
+
*
|
|
1367
|
+
* // Per-event metadata derived from the decision: entry i is written onto event i
|
|
1368
|
+
* // (`crypto` is the `Crypto.Crypto` service; `randomUUIDv7` works the same way)
|
|
1369
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1370
|
+
* eventMetadata: ({ events }) =>
|
|
1371
|
+
* Effect.forEach(events, () => Effect.map(crypto.randomULID, (eventId) => ({ eventId }))),
|
|
1372
|
+
* })
|
|
1373
|
+
*
|
|
1374
|
+
* // Per-event timestamps derived from the decision (undefined → the Clock time)
|
|
1375
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1376
|
+
* eventTimestamps: ({ events }) => events.map((event) => event.occurredAt),
|
|
1377
|
+
* })
|
|
1378
|
+
*
|
|
1379
|
+
* // ...or from a service, as an Effect (its errors / requirements join the handler's)
|
|
1380
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1381
|
+
* eventTimestamps: ({ events }) =>
|
|
1382
|
+
* Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId)),
|
|
1383
|
+
* })
|
|
403
1384
|
* ```
|
|
404
1385
|
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
1386
|
+
* Each invocation runs
|
|
1387
|
+
* `load → [expectedVersion check] → decide → fold new events → derive items → append → [snapshot]`.
|
|
1388
|
+
*
|
|
1389
|
+
* - **Load.** Strongly consistent by default — see
|
|
1390
|
+
* {@link CommandHandlerOptions.consistentRead}. When the stream declares a
|
|
1391
|
+
* `snapshot` config, state is loaded with {@link EventStream.readLatest} —
|
|
1392
|
+
* the snapshot, the events after it and the head in one request in steady
|
|
1393
|
+
* state — and only those events are folded, instead of replaying the stream
|
|
1394
|
+
* from the beginning. On a `mode: "inline"` stream without `every`,
|
|
1395
|
+
* {@link CommandHandlerOptions.verifySnapshot}`: false` loads the snapshot
|
|
1396
|
+
* item alone (one `GetItem`); an answer that is not a successful append is
|
|
1397
|
+
* confirmed against the head first, and a snapshot that proves stale falls
|
|
1398
|
+
* back to the verified load — in which case `decide` may run twice.
|
|
1399
|
+
* - **expectedVersion.** See {@link CommandOptionsBase.expectedVersion}.
|
|
1400
|
+
* - **Fold before append.** The new events are folded into state before the
|
|
1401
|
+
* append, so the state returned, snapshotted and handed to a function-form
|
|
1402
|
+
* `additionalItems` (see {@link Decision}) is always the `evolve` fold.
|
|
1403
|
+
* - **Snapshot.** With `snapshot.mode: "inline"`, the snapshot is written in
|
|
1404
|
+
* the append transaction itself — on every append, or at the `every`
|
|
1405
|
+
* cadence. With the default `"after-append"` and `snapshot.every` set, a
|
|
1406
|
+
* fresh snapshot is written (best-effort) after a successful append once the
|
|
1407
|
+
* cadence threshold is crossed. See {@link SnapshotMode}.
|
|
1408
|
+
* - **One atomic append.** One command, one decision, one append: a decision
|
|
1409
|
+
* too large for one transaction is rejected whole by DynamoDB and fails with
|
|
1410
|
+
* `AppendTooLarge` — nothing is written. A large command is split by the application into stepped
|
|
1411
|
+
* commands — each an ordinary call, chained by `expectedVersion` — so a
|
|
1412
|
+
* failure partway leaves the stream at a real intermediate state.
|
|
1413
|
+
*
|
|
1414
|
+
* The handler is generic per call over the error and requirements of a
|
|
1415
|
+
* function-form `additionalItems` that returns an `Effect`: they join the
|
|
1416
|
+
* handler's error channel and requirements (on a `BoundEventStream` handler
|
|
1417
|
+
* they are its only requirements).
|
|
409
1418
|
*
|
|
410
1419
|
* Without `idempotency`, command processing is **at-least-once**: a retry after
|
|
411
1420
|
* an acked-but-lost response re-runs `decide` and appends again.
|
|
@@ -417,12 +1426,13 @@ type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends Read
|
|
|
417
1426
|
* `EventStreamTypeId` brand of the second argument.
|
|
418
1427
|
*/
|
|
419
1428
|
export declare const commandHandler: {
|
|
420
|
-
<State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig):
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State,
|
|
425
|
-
|
|
1429
|
+
<State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig): <S extends BoundEventStream<TEvent, any, any, State, any, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
|
|
1430
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
|
|
1431
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
1432
|
+
}, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
1433
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
|
|
1434
|
+
readonly [K in TIndexName]: StreamIndexProjection;
|
|
1435
|
+
}, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
426
1436
|
};
|
|
427
1437
|
/**
|
|
428
1438
|
* Reconstruct state from events by folding through a decider's `evolve` function.
|