effect-dynamodb 1.22.0 → 1.24.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 +9 -4
- package/dist/Aggregate.d.ts.map +1 -1
- package/dist/Aggregate.js +644 -219
- package/dist/Aggregate.js.map +1 -1
- package/dist/Batch.d.ts +7 -0
- package/dist/Batch.d.ts.map +1 -1
- package/dist/Batch.js +132 -7
- package/dist/Batch.js.map +1 -1
- package/dist/Collection.d.ts.map +1 -1
- package/dist/Collection.js +13 -31
- package/dist/Collection.js.map +1 -1
- package/dist/DynamoClient.d.ts +40 -3
- package/dist/DynamoClient.d.ts.map +1 -1
- package/dist/DynamoClient.js +70 -19
- package/dist/DynamoClient.js.map +1 -1
- package/dist/Entity.d.ts +138 -55
- package/dist/Entity.d.ts.map +1 -1
- package/dist/Entity.js +3061 -626
- package/dist/Entity.js.map +1 -1
- package/dist/EventStore.d.ts +729 -53
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +1216 -231
- package/dist/EventStore.js.map +1 -1
- package/dist/Query.d.ts +75 -3
- package/dist/Query.d.ts.map +1 -1
- package/dist/Query.js +228 -30
- package/dist/Query.js.map +1 -1
- package/dist/Transaction.d.ts +13 -2
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +48 -38
- package/dist/Transaction.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/BoundCrud.d.ts +31 -20
- package/dist/internal/BoundCrud.d.ts.map +1 -1
- package/dist/internal/BoundCrud.js +1 -1
- package/dist/internal/BoundCrud.js.map +1 -1
- package/dist/internal/BoundQuery.d.ts +47 -2
- package/dist/internal/BoundQuery.d.ts.map +1 -1
- package/dist/internal/BoundQuery.js +182 -18
- package/dist/internal/BoundQuery.js.map +1 -1
- package/dist/internal/EntityCombinators.d.ts +32 -10
- package/dist/internal/EntityCombinators.d.ts.map +1 -1
- package/dist/internal/EntityCombinators.js +16 -7
- package/dist/internal/EntityCombinators.js.map +1 -1
- package/dist/internal/EntityOps.d.ts +44 -10
- package/dist/internal/EntityOps.d.ts.map +1 -1
- package/dist/internal/EntityOps.js +48 -5
- package/dist/internal/EntityOps.js.map +1 -1
- package/dist/internal/Expr.d.ts +23 -0
- package/dist/internal/Expr.d.ts.map +1 -1
- package/dist/internal/Expr.js +84 -9
- package/dist/internal/Expr.js.map +1 -1
- package/dist/internal/ItemSize.d.ts +38 -0
- package/dist/internal/ItemSize.d.ts.map +1 -0
- package/dist/internal/ItemSize.js +62 -0
- package/dist/internal/ItemSize.js.map +1 -0
- package/dist/internal/TransactWriteOps.d.ts +112 -19
- package/dist/internal/TransactWriteOps.d.ts.map +1 -1
- package/dist/internal/TransactWriteOps.js +210 -33
- package/dist/internal/TransactWriteOps.js.map +1 -1
- package/dist/internal/TransactableOps.d.ts +33 -14
- package/dist/internal/TransactableOps.d.ts.map +1 -1
- package/dist/internal/TransactableOps.js +46 -19
- package/dist/internal/TransactableOps.js.map +1 -1
- package/package.json +2 -2
package/dist/EventStore.d.ts
CHANGED
|
@@ -4,17 +4,25 @@
|
|
|
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 — `readIndex`, `query.index`,
|
|
17
|
+
* and `indexDefinitions` for the `CreateTable` fragments
|
|
11
18
|
* - `fold` / `foldFrom` helpers for state reconstruction
|
|
12
19
|
*
|
|
13
20
|
* Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
|
|
14
21
|
* DynamoClient, Marshaller).
|
|
15
22
|
*/
|
|
23
|
+
import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
|
|
16
24
|
import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
|
|
17
|
-
import { AdditionalItemConditionFailed, AppendTooLarge, DuplicateCommand, TransactionCancelled, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
|
|
25
|
+
import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
|
|
18
26
|
import { type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
|
|
19
27
|
import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
|
|
20
28
|
import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
|
|
@@ -24,8 +32,13 @@ import { type Table, type TableConfig } from "./Table.js";
|
|
|
24
32
|
* A Decider encodes the command-event-state triad for an aggregate.
|
|
25
33
|
*
|
|
26
34
|
* - `decide` — given a command and current state, produce events (or fail with E)
|
|
27
|
-
* - `evolve` —
|
|
28
|
-
*
|
|
35
|
+
* - `evolve` — left fold: apply one event to a state. It may return new state or
|
|
36
|
+
* mutate the given state in place and return it; the library requires
|
|
37
|
+
* neither. State the library persists, returns or projects is always this
|
|
38
|
+
* fold, never anything produced inside `decide`.
|
|
39
|
+
* - `initialState` — starting state for a new aggregate. An `evolve` that
|
|
40
|
+
* mutates in place must not mutate a shared `initialState` object (declare it
|
|
41
|
+
* as a getter that returns a fresh value).
|
|
29
42
|
*/
|
|
30
43
|
export interface Decider<State, Command, Event, E = never> {
|
|
31
44
|
readonly decide: (command: Command, state: State) => Effect.Effect<ReadonlyArray<Event>, E>;
|
|
@@ -80,16 +93,52 @@ export interface Snapshot<State> {
|
|
|
80
93
|
* - `schema` — the state codec. Snapshot state round-trips through it
|
|
81
94
|
* (`Schema.encodeUnknownEffect` on write, `Schema.decodeUnknownEffect` on
|
|
82
95
|
* read), so transforming schemas work.
|
|
83
|
-
* - `
|
|
84
|
-
*
|
|
85
|
-
*
|
|
96
|
+
* - `mode` — how {@link commandHandler} writes snapshots (see
|
|
97
|
+
* {@link SnapshotMode}). Default `"after-append"`.
|
|
98
|
+
* - `every` — optional snapshot cadence for {@link commandHandler}: write a
|
|
99
|
+
* fresh snapshot once at least this many events have accumulated since the
|
|
100
|
+
* last one. Must be a positive integer. With `mode: "after-append"` and no
|
|
101
|
+
* `every`, the handler never snapshots; with `mode: "inline"` and no `every`,
|
|
102
|
+
* it snapshots on every append.
|
|
103
|
+
*
|
|
104
|
+
* `every` also sizes the first page of {@link EventStream.readLatest}, which
|
|
105
|
+
* {@link commandHandler} loads state with: a snapshot that lags the head by up
|
|
106
|
+
* to `every` events still loads in one request. The first page is read whatever
|
|
107
|
+
* the actual lag, so every load reads up to `every + 1` items (`2` without
|
|
108
|
+
* `every`), including events the snapshot already covers, which are discarded.
|
|
109
|
+
* For an `"after-append"` stream that trades read capacity for requests: with
|
|
110
|
+
* `every: 100` and a snapshot one event behind, a load reads the snapshot and
|
|
111
|
+
* 100 events in one request, where a `GetItem` plus `readFrom` would read the
|
|
112
|
+
* snapshot and one event in two. Size `every` with that in mind, or use
|
|
113
|
+
* `mode: "inline"`, which keeps the snapshot current so the page stays small.
|
|
86
114
|
*/
|
|
87
115
|
export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
|
|
116
|
+
/**
|
|
117
|
+
* The state schema. Snapshot state is encoded through it, falling back to
|
|
118
|
+
* `decode → encode` (as events are) for a value not already in its type
|
|
119
|
+
* shape — so a `Schema.Class` state folded by an immutable `evolve` that
|
|
120
|
+
* spreads (`({ ...s, balance })`, a plain object) still encodes.
|
|
121
|
+
*/
|
|
88
122
|
readonly schema: TSchema;
|
|
123
|
+
readonly mode?: SnapshotMode | undefined;
|
|
89
124
|
readonly every?: number | undefined;
|
|
90
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* How {@link commandHandler} writes snapshots.
|
|
128
|
+
*
|
|
129
|
+
* - `"after-append"` (default) — a best-effort `writeSnapshot` after the append
|
|
130
|
+
* succeeds, at the `every` cadence. A failed snapshot write never fails the
|
|
131
|
+
* command; the snapshot simply lags until the next threshold crossing.
|
|
132
|
+
* - `"inline"` — the snapshot `Put` rides in the append's own transaction (see
|
|
133
|
+
* {@link AppendOptions.snapshot}), on every append, or at the `every` cadence
|
|
134
|
+
* when one is set. The snapshot is then current after every command that
|
|
135
|
+
* writes one, so {@link EventStream.readLatest} loads state in one request.
|
|
136
|
+
* The snapshot counts towards the transaction's item and size limits.
|
|
137
|
+
*/
|
|
138
|
+
export type SnapshotMode = "after-append" | "inline";
|
|
91
139
|
/** The runtime snapshot settings exposed on a stream. */
|
|
92
140
|
export interface SnapshotSettings {
|
|
141
|
+
readonly mode: SnapshotMode;
|
|
93
142
|
readonly every: number | undefined;
|
|
94
143
|
}
|
|
95
144
|
/**
|
|
@@ -100,6 +149,12 @@ export interface SnapshotSettings {
|
|
|
100
149
|
* `commandId` therefore cancels the whole transaction and surfaces as
|
|
101
150
|
* `DuplicateCommand` — the events are never written twice.
|
|
102
151
|
*
|
|
152
|
+
* The sentinel is consulted only by an append. Through `commandHandler`, a
|
|
153
|
+
* redelivery whose `decide` returns no events against the loaded state never
|
|
154
|
+
* appends, so it succeeds as a no-op instead of failing with
|
|
155
|
+
* `DuplicateCommand`. Whether a no-op redelivery needs to be told apart is the
|
|
156
|
+
* application's call (see {@link CommandOptions.commandId}).
|
|
157
|
+
*
|
|
103
158
|
* The sentinel is co-located in the stream's own partition, so `commandId`
|
|
104
159
|
* uniqueness is scoped to the stream (which is what "have I already applied this
|
|
105
160
|
* command to this aggregate?" asks). It is invisible to `read` / `readFrom` /
|
|
@@ -122,8 +177,13 @@ export interface AppendIdempotency {
|
|
|
122
177
|
*/
|
|
123
178
|
readonly ttl?: Duration.Duration | string;
|
|
124
179
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Options accepted by {@link EventStream.append}.
|
|
182
|
+
*
|
|
183
|
+
* `TState` is the stream's snapshot state type (`never` for a stream without a
|
|
184
|
+
* `snapshot` config, so `snapshot` cannot be supplied there).
|
|
185
|
+
*/
|
|
186
|
+
export interface AppendOptions<TMetadata, TState = never> {
|
|
127
187
|
/** Per-append metadata, validated against the stream's metadata schema when configured. */
|
|
128
188
|
readonly metadata?: TMetadata;
|
|
129
189
|
/**
|
|
@@ -131,16 +191,215 @@ export interface AppendOptions<TMetadata> {
|
|
|
131
191
|
* op union `Transaction.transactWrite` accepts (`EntityPut`, `EntityDelete`,
|
|
132
192
|
* `Transaction.check(...)`).
|
|
133
193
|
*
|
|
134
|
-
*
|
|
135
|
-
* `
|
|
136
|
-
*
|
|
194
|
+
* A put of a versioned or unique-constrained entity is written exactly as
|
|
195
|
+
* the entity's own `put` writes it (#133) — reading the item, continuing a
|
|
196
|
+
* replaced item's version and history, rotating its sentinels — and an
|
|
197
|
+
* append whose read raced a concurrent write is written again.
|
|
198
|
+
*
|
|
199
|
+
* A failure of an item's OWN condition (`.condition()`, `create()`,
|
|
200
|
+
* `Transaction.check`) is reported as `AdditionalItemConditionFailed`
|
|
201
|
+
* (carrying the 0-based indices into this array), never as
|
|
202
|
+
* `VersionConflict`. A guarded put the caller set no condition on fails as
|
|
203
|
+
* the entity's put would: `UniqueConstraintViolation`, a history
|
|
204
|
+
* `ValidationError`, or — a race lost on every attempt —
|
|
205
|
+
* `OptimisticLockError` / `ConcurrentModification`.
|
|
137
206
|
*/
|
|
138
207
|
readonly additionalItems?: ReadonlyArray<TransactWriteOp>;
|
|
139
208
|
/** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
|
|
140
209
|
readonly idempotency?: AppendIdempotency;
|
|
210
|
+
/**
|
|
211
|
+
* An inline snapshot: the stream's state as of this append's last event.
|
|
212
|
+
*
|
|
213
|
+
* The snapshot `Put` (`asOfVersion` = `expectedVersion + events.length`,
|
|
214
|
+
* encoded through the state schema) joins the **same transaction** as the
|
|
215
|
+
* events, after the idempotency sentinel, so it commits if and only if the
|
|
216
|
+
* events do. The `Put` is unconditional: the event puts already prove this
|
|
217
|
+
* writer owns `asOfVersion`, so it cannot regress the snapshot. It counts
|
|
218
|
+
* towards the item and size limits, and a cancellation reason at its
|
|
219
|
+
* position (throttling, for example) reports `TransactionCancelled`.
|
|
220
|
+
*
|
|
221
|
+
* `undefined` means no snapshot. Requires at least one event — with none,
|
|
222
|
+
* nothing proves the writer owns the version, and the append fails with
|
|
223
|
+
* `ValidationError`. On a stream without a `snapshot` config (where the type
|
|
224
|
+
* is `never`) it dies with `[EDD-9026]`, as `writeSnapshot` does.
|
|
225
|
+
*/
|
|
226
|
+
readonly snapshot?: TState;
|
|
141
227
|
}
|
|
142
228
|
/** Error channel of {@link EventStream.append}. */
|
|
143
|
-
export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled;
|
|
229
|
+
export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
230
|
+
/**
|
|
231
|
+
* Options accepted by {@link EventStream.read}, {@link EventStream.readFrom} and
|
|
232
|
+
* {@link EventStream.currentVersion}.
|
|
233
|
+
*
|
|
234
|
+
* `consistentRead: true` sets `ConsistentRead` on every `Query` page, so the
|
|
235
|
+
* read observes every append acknowledged before it started (read-your-writes).
|
|
236
|
+
* Default `false`: eventually consistent, at half the read cost.
|
|
237
|
+
*
|
|
238
|
+
* {@link commandHandler} loads state with strongly consistent reads by default —
|
|
239
|
+
* see {@link CommandHandlerOptions.consistentRead}.
|
|
240
|
+
*/
|
|
241
|
+
export interface ReadOptions {
|
|
242
|
+
readonly consistentRead?: boolean | undefined;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Options accepted by {@link EventStream.readLatest}: {@link ReadOptions} plus
|
|
246
|
+
* `verifySnapshot`.
|
|
247
|
+
*
|
|
248
|
+
* - `verifySnapshot` — default `true`: one `Query` reads the snapshot and the
|
|
249
|
+
* events after it, so the result is verified against the stream head.
|
|
250
|
+
* `false` reads **only the snapshot item**, with one `GetItem` (consistent
|
|
251
|
+
* per `consistentRead`), and returns
|
|
252
|
+
* `{ snapshot, events: [], version: snapshot.asOfVersion }` **without
|
|
253
|
+
* checking that no event follows it** — `version` is then the snapshot's
|
|
254
|
+
* version, not a verified head. When there is no snapshot item, it falls
|
|
255
|
+
* back to the verified read and returns that result.
|
|
256
|
+
*
|
|
257
|
+
* Why: a `Query`'s read capacity counts every item it reads, and the
|
|
258
|
+
* verified read's first page is the snapshot **plus** the newest event,
|
|
259
|
+
* even when the snapshot is current; a `GetItem` reads the snapshot alone.
|
|
260
|
+
* With large events that roughly halves the read capacity of a load.
|
|
261
|
+
*
|
|
262
|
+
* Only a stream whose snapshot config is `mode: "inline"` **without**
|
|
263
|
+
* `every` (the only mode that writes the snapshot in every append, so it is
|
|
264
|
+
* normally at the head) accepts `false`; any other snapshot config dies with
|
|
265
|
+
* `[EDD-9068]`. On a stream without a `snapshot` config the option is
|
|
266
|
+
* ignored. {@link commandHandler} exposes the same trade-off, with
|
|
267
|
+
* automatic fallbacks, as {@link CommandHandlerOptions.verifySnapshot}.
|
|
268
|
+
*/
|
|
269
|
+
export interface ReadLatestOptions extends ReadOptions {
|
|
270
|
+
readonly verifySnapshot?: boolean | undefined;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A stream's latest state as {@link EventStream.readLatest} loads it.
|
|
274
|
+
*
|
|
275
|
+
* - `snapshot` — the stream's snapshot, if one exists (always `None` on a
|
|
276
|
+
* stream without a `snapshot` config).
|
|
277
|
+
* - `events` — the events after the snapshot (every event when there is
|
|
278
|
+
* none), ascending by version. Fold them onto the snapshot's state.
|
|
279
|
+
* - `version` — the stream head: the newest event's version, the snapshot's
|
|
280
|
+
* `asOfVersion` when no event follows it, `0` for an empty stream. With
|
|
281
|
+
* `verifySnapshot: false` (see {@link ReadLatestOptions}) and a snapshot
|
|
282
|
+
* present, it is the snapshot's `asOfVersion` and `events` is empty —
|
|
283
|
+
* **unverified**: events appended without a snapshot (a raw `append`, data
|
|
284
|
+
* written before `mode: "inline"`) may follow it.
|
|
285
|
+
*/
|
|
286
|
+
export interface LatestState<TState, TEvent, M = Record<string, unknown> | undefined> {
|
|
287
|
+
readonly snapshot: Option.Option<Snapshot<TState>>;
|
|
288
|
+
readonly events: ReadonlyArray<StreamEvent<TEvent, M>>;
|
|
289
|
+
readonly version: number;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Derives an event's key in a stream index from the domain event (before it is
|
|
293
|
+
* encoded) and the version it is appended at.
|
|
294
|
+
*
|
|
295
|
+
* - A string is the event's sort key in the index. It is stored **raw**: no
|
|
296
|
+
* casing and no prefixing is applied, so compose it to sort the way you
|
|
297
|
+
* query (zero-pad numbers, for example).
|
|
298
|
+
* - `undefined` leaves the event out of the index (a sparse index).
|
|
299
|
+
* - An empty string (or any other value) fails the append with
|
|
300
|
+
* `ValidationError` before anything is written — DynamoDB rejects empty key
|
|
301
|
+
* values. So do a key over DynamoDB's 1024-byte sort-key limit and a `key`
|
|
302
|
+
* that throws.
|
|
303
|
+
*
|
|
304
|
+
* The key is derived only when an event is appended. Events are never
|
|
305
|
+
* rewritten, so changing `key` later leaves events already stored under their
|
|
306
|
+
* old keys (or out of the index), and declaring an index on a stream that
|
|
307
|
+
* already holds events indexes only the events appended from then on. The
|
|
308
|
+
* library does not backfill: re-deriving and writing the index attributes on
|
|
309
|
+
* existing events is the application's decision (see `indexes` on
|
|
310
|
+
* {@link makeStream}).
|
|
311
|
+
*/
|
|
312
|
+
export type StreamIndexKey<TEvent> = (event: TEvent, version: number) => string | undefined;
|
|
313
|
+
/**
|
|
314
|
+
* A stream index on a **local** secondary index (the default `type`). An LSI
|
|
315
|
+
* shares the table's partition key (`pk`), so its items are exactly the
|
|
316
|
+
* stream's indexed events, readable with strongly consistent reads.
|
|
317
|
+
*
|
|
318
|
+
* - `index` — the physical LSI name.
|
|
319
|
+
* - `sk` — the attribute carrying the derived key (the LSI's sort key).
|
|
320
|
+
* - `key` — see {@link StreamIndexKey}.
|
|
321
|
+
*
|
|
322
|
+
* An LSI must be created with the table. A table with any LSI caps the item
|
|
323
|
+
* collection of **every** partition key value in it at 10 GB — not only the
|
|
324
|
+
* indexed stream's partitions, but every stream's (indexed or not) and every
|
|
325
|
+
* entity partition sharing the table; a write that would exceed it fails. Its
|
|
326
|
+
* projection must be `ALL`, because events are decoded from the index item —
|
|
327
|
+
* see {@link indexDefinitions}.
|
|
328
|
+
*/
|
|
329
|
+
export interface LocalStreamIndexConfig<TEvent> {
|
|
330
|
+
readonly type?: "lsi" | undefined;
|
|
331
|
+
readonly index: string;
|
|
332
|
+
readonly sk: string;
|
|
333
|
+
/** An LSI uses the table partition key; supplying `pk` is refused (`[EDD-9063]`). */
|
|
334
|
+
readonly pk?: undefined;
|
|
335
|
+
readonly key: StreamIndexKey<TEvent>;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* A stream index on a **global** secondary index. `append` writes the stream's
|
|
339
|
+
* partition key value into the `pk` attribute of each indexed event, so the
|
|
340
|
+
* index is scoped to the same stream: the eventually consistent equivalent of
|
|
341
|
+
* an LSI, without the LSI's 10 GB item-collection cap. A strongly consistent
|
|
342
|
+
* read of it is refused with `ValidationError`.
|
|
343
|
+
*
|
|
344
|
+
* Unlike an LSI, the GSI itself can be added to an existing table
|
|
345
|
+
* (`UpdateTable`), but that does not index the stream's existing events: the
|
|
346
|
+
* index attributes are written only by `append`, so only events appended after
|
|
347
|
+
* the index is declared are in it — see {@link StreamIndexKey}.
|
|
348
|
+
*
|
|
349
|
+
* - `index` — the physical GSI name.
|
|
350
|
+
* - `pk` — the attribute carrying the stream's partition key (the GSI's HASH key).
|
|
351
|
+
* - `sk` — the attribute carrying the derived key (the GSI's RANGE key).
|
|
352
|
+
* - `key` — see {@link StreamIndexKey}.
|
|
353
|
+
*/
|
|
354
|
+
export interface GlobalStreamIndexConfig<TEvent> {
|
|
355
|
+
readonly type: "gsi";
|
|
356
|
+
readonly index: string;
|
|
357
|
+
readonly pk: string;
|
|
358
|
+
readonly sk: string;
|
|
359
|
+
readonly key: StreamIndexKey<TEvent>;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* One entry of `makeStream({ indexes })`: a sub-stream of the stream's events,
|
|
363
|
+
* ordered by a key derived from each event. See {@link LocalStreamIndexConfig}
|
|
364
|
+
* and {@link GlobalStreamIndexConfig}.
|
|
365
|
+
*/
|
|
366
|
+
export type StreamIndexConfig<TEvent> = LocalStreamIndexConfig<TEvent> | GlobalStreamIndexConfig<TEvent>;
|
|
367
|
+
/**
|
|
368
|
+
* The runtime settings of one stream index, as exposed on
|
|
369
|
+
* {@link EventStream.indexes}. `pk` is the index's partition key attribute:
|
|
370
|
+
* the table's `pk` for an LSI, the configured attribute for a GSI.
|
|
371
|
+
*/
|
|
372
|
+
export interface StreamIndexSettings {
|
|
373
|
+
readonly type: "lsi" | "gsi";
|
|
374
|
+
readonly index: string;
|
|
375
|
+
readonly pk: string;
|
|
376
|
+
readonly sk: string;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Options accepted by {@link EventStream.readIndex}.
|
|
380
|
+
*
|
|
381
|
+
* - `beginsWith` / `between` — a sort-key condition on the derived key (at most
|
|
382
|
+
* one of them). Omitted, the whole sub-stream is read. Each bound must be a
|
|
383
|
+
* non-empty string, and `between`'s lower bound must not sort (by UTF-8
|
|
384
|
+
* bytes, as DynamoDB sorts) after its upper bound; otherwise the read fails
|
|
385
|
+
* with `ValidationError` before anything is sent.
|
|
386
|
+
* - `reverse` — descending key order.
|
|
387
|
+
* - `limit` — return at most this many events (a positive integer).
|
|
388
|
+
* - `consistentRead` — strongly consistent read. Refused with
|
|
389
|
+
* `ValidationError` on a GSI-backed index, which DynamoDB reads only
|
|
390
|
+
* eventually consistently.
|
|
391
|
+
*/
|
|
392
|
+
export type ReadIndexOptions = {
|
|
393
|
+
readonly reverse?: boolean | undefined;
|
|
394
|
+
readonly limit?: number | undefined;
|
|
395
|
+
readonly consistentRead?: boolean | undefined;
|
|
396
|
+
} & ({
|
|
397
|
+
readonly beginsWith?: string | undefined;
|
|
398
|
+
readonly between?: undefined;
|
|
399
|
+
} | {
|
|
400
|
+
readonly between?: readonly [string, string] | undefined;
|
|
401
|
+
readonly beginsWith?: undefined;
|
|
402
|
+
});
|
|
144
403
|
type StreamIdInput<T extends ReadonlyArray<string>> = {
|
|
145
404
|
readonly [K in T[number]]: string;
|
|
146
405
|
};
|
|
@@ -151,14 +410,28 @@ export type EventStreamTypeId = typeof EventStreamTypeId;
|
|
|
151
410
|
*
|
|
152
411
|
* Created via {@link makeStream}. Operations are called directly on the stream:
|
|
153
412
|
* `MatchEvents.append(...)`, `MatchEvents.read(...)`, `MatchEvents.query.events(...)`.
|
|
413
|
+
*
|
|
414
|
+
* `TIndexName` is the union of the stream's index names (`makeStream({ indexes })`),
|
|
415
|
+
* `never` for a stream without indexes, so {@link EventStream.readIndex} and
|
|
416
|
+
* `query.index` only accept a declared name.
|
|
154
417
|
*/
|
|
155
|
-
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never> extends Pipeable.Pipeable {
|
|
418
|
+
export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
|
|
156
419
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
157
420
|
readonly streamName: string;
|
|
158
421
|
readonly eventSchema: Schema.Top;
|
|
422
|
+
/**
|
|
423
|
+
* The stream's indexes (`makeStream({ indexes })`) by name — their physical
|
|
424
|
+
* index and key attributes. Empty for a stream without indexes.
|
|
425
|
+
* {@link indexDefinitions} derives `CreateTable` fragments from it.
|
|
426
|
+
*/
|
|
427
|
+
readonly indexes: {
|
|
428
|
+
readonly [K in TIndexName]: StreamIndexSettings;
|
|
429
|
+
};
|
|
159
430
|
/**
|
|
160
431
|
* Present iff the stream was created with a `snapshot` config. Its presence
|
|
161
|
-
* is what switches {@link commandHandler} onto the snapshot-aware read path
|
|
432
|
+
* is what switches {@link commandHandler} onto the snapshot-aware read path
|
|
433
|
+
* ({@link EventStream.readLatest}); its `mode` decides how the handler writes
|
|
434
|
+
* snapshots.
|
|
162
435
|
*/
|
|
163
436
|
readonly snapshotConfig: SnapshotSettings | undefined;
|
|
164
437
|
/**
|
|
@@ -187,12 +460,77 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
187
460
|
* hide state-schema evolution bugs.
|
|
188
461
|
*/
|
|
189
462
|
readSnapshot(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<Option.Option<Snapshot<TState>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
463
|
+
/**
|
|
464
|
+
* Append `events` after `expectedVersion`, atomically, in one transaction —
|
|
465
|
+
* see {@link AppendOptions}.
|
|
466
|
+
* A method, like `writeSnapshot`, so a snapshot-less stream (whose
|
|
467
|
+
* `options.snapshot` is `never`) stays assignable to other streams.
|
|
468
|
+
*/
|
|
469
|
+
append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, DynamoClient | TableConfig>;
|
|
470
|
+
/**
|
|
471
|
+
* Read every event of the stream, ascending by version. Pass
|
|
472
|
+
* `{ consistentRead: true }` for read-your-writes — see {@link ReadOptions}.
|
|
473
|
+
*/
|
|
474
|
+
read(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
475
|
+
/**
|
|
476
|
+
* Read the events after `afterVersion` (exclusive), ascending. Pass
|
|
477
|
+
* `{ consistentRead: true }` for read-your-writes — see {@link ReadOptions}.
|
|
478
|
+
*/
|
|
479
|
+
readFrom(streamId: StreamIdInput<TStreamIdFields>, afterVersion: number, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
480
|
+
/**
|
|
481
|
+
* The version of the stream's newest event (`0` for an empty stream), in one
|
|
482
|
+
* request. Pass `{ consistentRead: true }` for read-your-writes — see
|
|
483
|
+
* {@link ReadOptions}.
|
|
484
|
+
*/
|
|
485
|
+
currentVersion(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<number, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
486
|
+
/**
|
|
487
|
+
* The stream's snapshot (if any), the events after it, and its head — in
|
|
488
|
+
* **one request** in steady state. This is how {@link commandHandler} loads
|
|
489
|
+
* state on a snapshot-configured stream.
|
|
490
|
+
*
|
|
491
|
+
* Issues a single reverse `Query` over the sort-key range from the first
|
|
492
|
+
* event to the snapshot (the snapshot sorts after every event, and the
|
|
493
|
+
* idempotency sentinels before them, so the range holds exactly the events
|
|
494
|
+
* and the snapshot), with a first page of `(every ?? 1) + 1` items. Further
|
|
495
|
+
* pages are read only while the snapshot's `asOfVersion` has not been reached
|
|
496
|
+
* (a snapshot lagging by more than `every` events) — each sized to the events
|
|
497
|
+
* still missing, so a lagging snapshot costs one more request — or to the
|
|
498
|
+
* start of the stream when there is no snapshot. Pass
|
|
499
|
+
* `{ consistentRead: true }` for read-your-writes; it applies to the snapshot
|
|
500
|
+
* and the events alike.
|
|
501
|
+
*
|
|
502
|
+
* A snapshot that fails to decode through the state schema fails with
|
|
503
|
+
* `ValidationError`, as {@link EventStream.readSnapshot} does. On a stream
|
|
504
|
+
* without a `snapshot` config it is {@link EventStream.read} plus the head.
|
|
505
|
+
*
|
|
506
|
+
* `{ verifySnapshot: false }` reads only the snapshot item, with one
|
|
507
|
+
* `GetItem`, and returns it with no events and its `asOfVersion` as an
|
|
508
|
+
* **unverified** `version`; with no snapshot item it falls back to the
|
|
509
|
+
* verified read. Accepted only on a `mode: "inline"` stream without `every`
|
|
510
|
+
* (otherwise a defect, `[EDD-9068]`), ignored on a stream without a
|
|
511
|
+
* `snapshot` config — see {@link ReadLatestOptions}.
|
|
512
|
+
*/
|
|
513
|
+
readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
514
|
+
/**
|
|
515
|
+
* Read the stream's events in the order of one of its indexes (#140) — the
|
|
516
|
+
* events whose index `key` returned a string, ordered by that key. A
|
|
517
|
+
* convenience over `query.index`; pass `beginsWith` or `between` to narrow
|
|
518
|
+
* the key range, `reverse`, `limit` and `consistentRead` (refused on a GSI)
|
|
519
|
+
* — see {@link ReadIndexOptions}.
|
|
520
|
+
*/
|
|
521
|
+
readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
|
|
194
522
|
readonly query: {
|
|
195
523
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
524
|
+
/**
|
|
525
|
+
* A query of one of the stream's indexes (#140), scoped to `streamId`:
|
|
526
|
+
* decoded events in index-key order. It composes with the `Query`
|
|
527
|
+
* combinators — `Query.where` (a condition on the derived key),
|
|
528
|
+
* `Query.reverse`, `Query.limit`, `Query.consistentRead` (refused with
|
|
529
|
+
* `ValidationError` on a GSI-backed index) — and terminals
|
|
530
|
+
* (`Query.collect`, `Query.execute`, `Query.paginate`). The
|
|
531
|
+
* `__edd_e__ = <stream>.event` filter still applies.
|
|
532
|
+
*/
|
|
533
|
+
index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
196
534
|
};
|
|
197
535
|
}
|
|
198
536
|
/**
|
|
@@ -235,6 +573,10 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
235
573
|
* })
|
|
236
574
|
* ```
|
|
237
575
|
*
|
|
576
|
+
* `snapshot.mode: "inline"` writes the snapshot in the append transaction
|
|
577
|
+
* itself, so it is current after every command and state loads in one
|
|
578
|
+
* request — see {@link SnapshotMode}.
|
|
579
|
+
*
|
|
238
580
|
* `casing` overrides the key casing for this stream, as an index's or a vector
|
|
239
581
|
* index's `casing` does: the stream name, stream-id values and command ids in
|
|
240
582
|
* its keys all take it. When omitted, the stream name is lower-cased and the
|
|
@@ -245,11 +587,61 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
|
|
|
245
587
|
* (`<stream>.event` etc.) are always lower-cased. In the next major, omitting
|
|
246
588
|
* `casing` will mean the schema's casing, as it does for indexes.
|
|
247
589
|
*
|
|
590
|
+
* `indexes` declares sub-streams ordered by a key derived from each event
|
|
591
|
+
* (#140), on a local (default) or global secondary index. `append` writes each
|
|
592
|
+
* event's derived key (and, for a GSI, the stream's partition key) on the
|
|
593
|
+
* event item; snapshots and idempotency sentinels never carry them, so they
|
|
594
|
+
* are never in an index. Read with {@link EventStream.readIndex} or
|
|
595
|
+
* `query.index`, and create the indexes with {@link indexDefinitions}.
|
|
596
|
+
*
|
|
597
|
+
* Index attributes are written only at append time and events are never
|
|
598
|
+
* rewritten. Declaring an index on a stream that already holds events, or
|
|
599
|
+
* changing an index's `key`, therefore leaves the earlier events out of the
|
|
600
|
+
* index (or under their old keys), and `readIndex` returns only what was
|
|
601
|
+
* indexed — without an error. The library does not backfill; whether and how
|
|
602
|
+
* to re-index existing events (for example, an `UpdateItem` per event setting
|
|
603
|
+
* the derived attributes) is the application's decision.
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* const Entries = EventStore.makeStream({
|
|
608
|
+
* table: EventsTable,
|
|
609
|
+
* streamName: "Ledger",
|
|
610
|
+
* events: [EntryRecorded, EntryVoided],
|
|
611
|
+
* streamId: { composite: ["ledgerId"] },
|
|
612
|
+
* indexes: {
|
|
613
|
+
* byEntry: {
|
|
614
|
+
* index: "lsi1",
|
|
615
|
+
* sk: "lsi1sk",
|
|
616
|
+
* key: (event, version) =>
|
|
617
|
+
* event._tag === "EntryRecorded" ? `ENTRY#${event.section}#${pad(version)}` : undefined,
|
|
618
|
+
* },
|
|
619
|
+
* // Every event, by the day it was recorded (both event types carry `day`).
|
|
620
|
+
* byDay: { type: "gsi", index: "gsi1", pk: "gsi1pk", sk: "gsi1sk", key: (e) => e.day },
|
|
621
|
+
* },
|
|
622
|
+
* })
|
|
623
|
+
*
|
|
624
|
+
* const sectionThree = yield* Entries.readIndex("byEntry", { ledgerId }, { beginsWith: "ENTRY#3#" })
|
|
625
|
+
* ```
|
|
626
|
+
*
|
|
248
627
|
* @throws `[EDD-9027]` when `snapshot.every` is not a positive integer.
|
|
628
|
+
* @throws `[EDD-9062]` when `snapshot.mode` is neither `"after-append"` nor
|
|
629
|
+
* `"inline"`.
|
|
630
|
+
* @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
|
|
631
|
+
* `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, an empty `index` / `sk`
|
|
632
|
+
* / `pk`, or a `key` that is not a function.
|
|
633
|
+
* @throws `[EDD-9064]` when an index attribute is one the stream writes itself
|
|
634
|
+
* (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
|
|
635
|
+
* `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
|
|
636
|
+
* default TTL attribute `_ttl`). A custom `TableConfig.ttlAttributeName`
|
|
637
|
+
* is only known at runtime: an index attribute equal to it fails each append
|
|
638
|
+
* with `ValidationError`.
|
|
639
|
+
* @throws `[EDD-9065]` when two indexes share a physical index name or an
|
|
640
|
+
* attribute (or a GSI's `pk` and `sk` are the same attribute).
|
|
249
641
|
*/
|
|
250
642
|
export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top>, TTable extends Table, const TStreamName extends string, const TStreamId extends {
|
|
251
643
|
readonly composite: ReadonlyArray<string>;
|
|
252
|
-
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined>(config: {
|
|
644
|
+
}, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexName extends string = never>(config: {
|
|
253
645
|
readonly table: TTable;
|
|
254
646
|
readonly streamName: TStreamName;
|
|
255
647
|
readonly events: TEvents;
|
|
@@ -257,7 +649,8 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
257
649
|
readonly metadata?: TMetadata;
|
|
258
650
|
readonly snapshot?: TSnapshot;
|
|
259
651
|
readonly casing?: DynamoSchema.Casing | undefined;
|
|
260
|
-
|
|
652
|
+
readonly indexes?: { readonly [K in TIndexName]: StreamIndexConfig<Schema.Schema.Type<TEvents[number]>>; };
|
|
653
|
+
}) => 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, TIndexName>;
|
|
261
654
|
/**
|
|
262
655
|
* An EventStream whose operations have `DynamoClient` and `TableConfig` already
|
|
263
656
|
* resolved, so all methods return `Effect<A, E, never>`.
|
|
@@ -278,22 +671,40 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
|
|
|
278
671
|
* }) {}
|
|
279
672
|
* ```
|
|
280
673
|
*/
|
|
281
|
-
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never> extends Pipeable.Pipeable {
|
|
674
|
+
export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
|
|
282
675
|
readonly [EventStreamTypeId]: EventStreamTypeId;
|
|
283
676
|
readonly streamName: string;
|
|
284
677
|
readonly eventSchema: Schema.Top;
|
|
285
678
|
/** See {@link EventStream.snapshotConfig}. */
|
|
286
679
|
readonly snapshotConfig: SnapshotSettings | undefined;
|
|
680
|
+
/** See {@link EventStream.indexes}. */
|
|
681
|
+
readonly indexes: {
|
|
682
|
+
readonly [K in TIndexName]: StreamIndexSettings;
|
|
683
|
+
};
|
|
287
684
|
/** See {@link EventStream.writeSnapshot} — a method for the same variance reason. */
|
|
288
685
|
writeSnapshot(streamId: StreamIdInput<TStreamIdFields>, state: TState, asOfVersion: number): Effect.Effect<void, DynamoClientError | ValidationError, never>;
|
|
289
686
|
/** See {@link EventStream.readSnapshot}. */
|
|
290
687
|
readSnapshot(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<Option.Option<Snapshot<TState>>, DynamoClientError | ValidationError, never>;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
688
|
+
/** See {@link EventStream.append}. */
|
|
689
|
+
append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, never>;
|
|
690
|
+
/** See {@link EventStream.read}. */
|
|
691
|
+
read(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
|
|
692
|
+
/** See {@link EventStream.readFrom}. */
|
|
693
|
+
readFrom(streamId: StreamIdInput<TStreamIdFields>, afterVersion: number, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
|
|
694
|
+
/** See {@link EventStream.currentVersion}. */
|
|
695
|
+
currentVersion(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<number, DynamoClientError | ValidationError, never>;
|
|
696
|
+
/** See {@link EventStream.readLatest}. */
|
|
697
|
+
readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, never>;
|
|
698
|
+
/** See {@link EventStream.readIndex}. */
|
|
699
|
+
readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
|
|
700
|
+
/**
|
|
701
|
+
* The stream's query builders — unbound: run them with {@link provide}, or
|
|
702
|
+
* where `DynamoClient` and `TableConfig` are provided.
|
|
703
|
+
*/
|
|
295
704
|
readonly query: {
|
|
296
705
|
events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
706
|
+
/** See `EventStream.query.index`. */
|
|
707
|
+
index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
|
|
297
708
|
};
|
|
298
709
|
/** Escape hatch: provide DynamoClient | TableConfig to an arbitrary effect. */
|
|
299
710
|
readonly provide: <A, E>(effect: Effect.Effect<A, E, DynamoClient | TableConfig>) => Effect.Effect<A, E, never>;
|
|
@@ -312,7 +723,64 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
|
|
|
312
723
|
* yield* stream.append({ matchId: "m-1" }, [event], 0) // R = never
|
|
313
724
|
* ```
|
|
314
725
|
*/
|
|
315
|
-
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>;
|
|
726
|
+
export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState, TIndexName extends string = never>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>, never, DynamoClient | TableConfig>;
|
|
727
|
+
/**
|
|
728
|
+
* `CreateTable` fragments for the stream indexes of one or more streams — see
|
|
729
|
+
* {@link indexDefinitions}. The lists are absent when empty, because
|
|
730
|
+
* DynamoDB rejects an empty index list.
|
|
731
|
+
*/
|
|
732
|
+
export interface StreamIndexDefinitions {
|
|
733
|
+
/** The index key attributes (all strings), excluding the table's `pk` / `sk`. */
|
|
734
|
+
readonly AttributeDefinitions: Array<AttributeDefinition>;
|
|
735
|
+
readonly LocalSecondaryIndexes?: Array<LocalSecondaryIndex> | undefined;
|
|
736
|
+
readonly GlobalSecondaryIndexes?: Array<GlobalSecondaryIndex> | undefined;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Derive the `CreateTable` fragments for the stream indexes (#140) of the given
|
|
740
|
+
* streams (`EventStream` or `BoundEventStream`) — `AttributeDefinitions`,
|
|
741
|
+
* `LocalSecondaryIndexes` and `GlobalSecondaryIndexes`, each index projecting
|
|
742
|
+
* `ALL` — to merge into the caller's `CreateTable` input. Event tables are not
|
|
743
|
+
* derived by `Table.definition`, and the table's own `pk` / `sk` are not
|
|
744
|
+
* included: add them (and, on a provisioned table, each GSI's
|
|
745
|
+
* `ProvisionedThroughput`) yourself.
|
|
746
|
+
*
|
|
747
|
+
* An index shared by several streams with the same definition is emitted
|
|
748
|
+
* once. Lists are sorted by index name, attributes by attribute name.
|
|
749
|
+
*
|
|
750
|
+
* - An LSI can only be created with the table, and caps the item collection
|
|
751
|
+
* of every partition key value in the table at 10 GB — entity partitions
|
|
752
|
+
* and streams without indexes included.
|
|
753
|
+
* - The projection must stay `ALL`: events are decoded from the index item.
|
|
754
|
+
*
|
|
755
|
+
* @example
|
|
756
|
+
* ```typescript
|
|
757
|
+
* const fragments = EventStore.indexDefinitions(Entries, Orders)
|
|
758
|
+
* yield* client.createTable({
|
|
759
|
+
* TableName: "events",
|
|
760
|
+
* BillingMode: "PAY_PER_REQUEST",
|
|
761
|
+
* KeySchema: [
|
|
762
|
+
* { AttributeName: "pk", KeyType: "HASH" },
|
|
763
|
+
* { AttributeName: "sk", KeyType: "RANGE" },
|
|
764
|
+
* ],
|
|
765
|
+
* AttributeDefinitions: [
|
|
766
|
+
* { AttributeName: "pk", AttributeType: "S" },
|
|
767
|
+
* { AttributeName: "sk", AttributeType: "S" },
|
|
768
|
+
* ...fragments.AttributeDefinitions,
|
|
769
|
+
* ],
|
|
770
|
+
* LocalSecondaryIndexes: fragments.LocalSecondaryIndexes,
|
|
771
|
+
* GlobalSecondaryIndexes: fragments.GlobalSecondaryIndexes,
|
|
772
|
+
* })
|
|
773
|
+
* ```
|
|
774
|
+
*
|
|
775
|
+
* @throws `[EDD-9067]` when two streams define the same physical index
|
|
776
|
+
* differently (another type, or other key attributes).
|
|
777
|
+
*/
|
|
778
|
+
export declare const indexDefinitions: (...streams: ReadonlyArray<{
|
|
779
|
+
readonly streamName: string;
|
|
780
|
+
readonly indexes: {
|
|
781
|
+
readonly [name: string]: StreamIndexSettings;
|
|
782
|
+
};
|
|
783
|
+
}>) => StreamIndexDefinitions;
|
|
316
784
|
/**
|
|
317
785
|
* Handler-level configuration for {@link commandHandler}.
|
|
318
786
|
*
|
|
@@ -327,32 +795,188 @@ export interface CommandHandlerOptions {
|
|
|
327
795
|
* The retried unit is the entire read–decide–append cycle, so every attempt
|
|
328
796
|
* decides against freshly read state — a blind re-append of stale events is
|
|
329
797
|
* impossible by construction. Snapshot reads participate: a retried attempt
|
|
330
|
-
* re-reads the snapshot and its delta
|
|
798
|
+
* re-reads the snapshot and its delta (the snapshot alone with
|
|
799
|
+
* {@link verifySnapshot}`: false`), and a function-form `additionalItems` is
|
|
800
|
+
* re-evaluated against the new decision.
|
|
331
801
|
*
|
|
332
802
|
* A number `n` is shorthand for `Schedule.recurs(n)` (n retries *after* the
|
|
333
803
|
* initial attempt). Omit for the default: no retry.
|
|
334
804
|
*
|
|
335
|
-
* `DuplicateCommand` is deliberately NOT retried — it is terminal.
|
|
805
|
+
* `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
|
|
806
|
+
* any call that supplies {@link CommandOptions.expectedVersion}: the caller asked
|
|
807
|
+
* for a conditional write, so its `VersionConflict` is always surfaced.
|
|
336
808
|
*/
|
|
337
809
|
readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
|
|
338
810
|
/** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
|
|
339
811
|
readonly idempotency?: {
|
|
340
812
|
readonly ttl?: Duration.Duration | string;
|
|
341
813
|
};
|
|
814
|
+
/**
|
|
815
|
+
* Load state with strongly consistent reads. Default `true`.
|
|
816
|
+
*
|
|
817
|
+
* An eventually consistent load can miss the newest events, so `decide` runs
|
|
818
|
+
* against stale state and the append then fails with `VersionConflict` (or
|
|
819
|
+
* burns a retry). A consistent load costs twice the read capacity of an
|
|
820
|
+
* eventually consistent one. Set `false` to trade that for the occasional
|
|
821
|
+
* conflict. On a snapshot-configured stream the snapshot and its delta are
|
|
822
|
+
* one query ({@link EventStream.readLatest}), so the setting covers both.
|
|
823
|
+
*/
|
|
824
|
+
readonly consistentRead?: boolean | undefined;
|
|
825
|
+
/**
|
|
826
|
+
* Verify the snapshot against the stream head when loading state. Default
|
|
827
|
+
* `true`.
|
|
828
|
+
*
|
|
829
|
+
* With `true`, a snapshot-configured stream loads with one `Query` that
|
|
830
|
+
* reads the snapshot **and** the newest event (more on a lagging snapshot),
|
|
831
|
+
* so the state `decide` sees is verified current. `false` loads with one
|
|
832
|
+
* `GetItem` of the snapshot item only ({@link EventStream.readLatest} with
|
|
833
|
+
* `verifySnapshot: false`). A `Query`'s read capacity counts every item it
|
|
834
|
+
* reads, a `GetItem` only the snapshot, so with large events `false`
|
|
835
|
+
* roughly halves the read capacity per command.
|
|
836
|
+
*
|
|
837
|
+
* Accepted only when the stream's snapshot config is `mode: "inline"`
|
|
838
|
+
* **without** `every` — the only mode that writes the snapshot in every
|
|
839
|
+
* append's transaction, so it is normally at the head. Any other snapshot
|
|
840
|
+
* config throws `[EDD-9068]` when the handler is created (data-first or
|
|
841
|
+
* data-last). On a stream without a `snapshot` config the option is
|
|
842
|
+
* ignored: state is replayed and verified as always.
|
|
843
|
+
*
|
|
844
|
+
* The snapshot can still lag the head — events appended without one (a raw
|
|
845
|
+
* `append`, data written before `mode: "inline"`). So no answer made on an
|
|
846
|
+
* unverified snapshot is returned until the head confirms it: a successful
|
|
847
|
+
* append confirms it by itself; anything else — a `decide` failure, a no-op
|
|
848
|
+
* decision, an append `VersionConflict` — is checked with one more read,
|
|
849
|
+
* and a stale load falls back to a verified one:
|
|
850
|
+
*
|
|
851
|
+
* - **No snapshot item** (a stream not yet snapshotted): the verified load
|
|
852
|
+
* runs at once, and `decide` runs once.
|
|
853
|
+
* - **With {@link CommandOptions.expectedVersion}**: a snapshot whose
|
|
854
|
+
* `asOfVersion` differs from it is re-read verified before the If-Match
|
|
855
|
+
* check, so only a genuine mismatch is reported (its `actualVersion` is
|
|
856
|
+
* the verified head). A snapshot at the expected version holds exactly the
|
|
857
|
+
* state the caller saw, so `decide` runs on it; if that decision does not
|
|
858
|
+
* append, one head `Query` above the If-Match checks the head (see
|
|
859
|
+
* below). A head past the
|
|
860
|
+
* If-Match discards the decision and answers as a verified load would:
|
|
861
|
+
* `DuplicateCommand` for a committed redelivery, else `VersionConflict`
|
|
862
|
+
* with the head as `actualVersion` — never re-decided or retried.
|
|
863
|
+
* - **Without it**, a `decide` failure or a no-op is checked against the
|
|
864
|
+
* head the same way and returned when the head is at the snapshot. An
|
|
865
|
+
* append that fails with `VersionConflict` triggers one verified load:
|
|
866
|
+
* a snapshot at the head means another writer won a genuine race, and
|
|
867
|
+
* the `VersionConflict` goes to the {@link retry} policy as it always
|
|
868
|
+
* does. Otherwise — the head past the snapshot, or past the version
|
|
869
|
+
* decided on — the first decision was made on stale state and is
|
|
870
|
+
* discarded: the handler loads verified (if it has not) and decides again
|
|
871
|
+
* on that state, then appends — **`decide` (and a function-form
|
|
872
|
+
* `additionalItems`, for a decision that appends) runs twice for that
|
|
873
|
+
* call**. That is the optimistic-concurrency price of skipping the
|
|
874
|
+
* verification read, the same one a `VersionConflict` retry pays: a
|
|
875
|
+
* decision is only as current as the state it was made on, and a stale
|
|
876
|
+
* one cannot be patched, only made again. Without the re-decision, a
|
|
877
|
+
* lagging snapshot would fail or mis-answer every command on the stream,
|
|
878
|
+
* since only a successful inline append rewrites it. It happens at most
|
|
879
|
+
* once per attempt, and the second decision's outcome is final for the
|
|
880
|
+
* attempt (its conflict follows the policy).
|
|
881
|
+
*
|
|
882
|
+
* The head check is a reverse `Limit: 1` `Query` bounded below by the
|
|
883
|
+
* snapshot's version (or the If-Match), never a read of the newest event:
|
|
884
|
+
* while the snapshot is current the range is empty, so it reads no items
|
|
885
|
+
* and costs only DynamoDB's per-request minimum, however large the events
|
|
886
|
+
* are. A decision that appends nothing therefore costs one extra round trip,
|
|
887
|
+
* not extra item reads.
|
|
888
|
+
*/
|
|
889
|
+
readonly verifySnapshot?: boolean | undefined;
|
|
342
890
|
}
|
|
343
|
-
/**
|
|
344
|
-
|
|
891
|
+
/**
|
|
892
|
+
* The outcome of one `decide` call, handed to a function-form
|
|
893
|
+
* {@link CommandOptions.additionalItems}.
|
|
894
|
+
*
|
|
895
|
+
* - `events` — what `decide` returned (never empty: the function is not called
|
|
896
|
+
* for a no-op decision).
|
|
897
|
+
* - `state` — the `evolve` fold of `previous` with `events`: the state the
|
|
898
|
+
* handler returns and snapshots.
|
|
899
|
+
* - `previous` — the state `decide` was given.
|
|
900
|
+
* - `version` — the stream version the events are appended after; the new
|
|
901
|
+
* events take `version + 1 … version + events.length`.
|
|
902
|
+
*
|
|
903
|
+
* `evolve` may mutate state in place. When it does, `previous` and `state` are
|
|
904
|
+
* the same reference and `previous` already reflects `events`; a projection
|
|
905
|
+
* that needs a pristine `previous` requires an `evolve` that returns new state.
|
|
906
|
+
*/
|
|
907
|
+
export interface Decision<State, Event> {
|
|
908
|
+
readonly events: ReadonlyArray<Event>;
|
|
909
|
+
readonly state: State;
|
|
910
|
+
readonly previous: State;
|
|
911
|
+
readonly version: number;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* The forms {@link CommandOptions.additionalItems} accepts: a static array of
|
|
915
|
+
* transact ops, or a function of the {@link Decision} returning the array
|
|
916
|
+
* directly (pure projection) or as an `Effect` (a projection that needs a read).
|
|
917
|
+
* The effect's `E2` joins the handler's error channel and its `R2` the handler's
|
|
918
|
+
* requirements.
|
|
919
|
+
*/
|
|
920
|
+
export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
|
|
921
|
+
/**
|
|
922
|
+
* Per-call options accepted by a handler produced by {@link commandHandler}.
|
|
923
|
+
*
|
|
924
|
+
* `State` / `Event` type the {@link Decision} a function-form `additionalItems`
|
|
925
|
+
* receives; `E2` / `R2` are the error and requirements of the `Effect` it may
|
|
926
|
+
* return. The handler infers all four, so they only need naming when this type
|
|
927
|
+
* is written out by hand.
|
|
928
|
+
*/
|
|
929
|
+
export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never> {
|
|
345
930
|
readonly metadata?: TMetadata;
|
|
346
931
|
/**
|
|
347
932
|
* Identifier for this command delivery. Required when the handler was created
|
|
348
|
-
* with `idempotency`; a replayed id fails with `DuplicateCommand
|
|
933
|
+
* with `idempotency`; a replayed id fails with `DuplicateCommand` — also when
|
|
934
|
+
* the replay carries an {@link expectedVersion} the stream has since moved
|
|
935
|
+
* past (see there).
|
|
936
|
+
*
|
|
937
|
+
* Otherwise the sentinel is consulted when the events are appended, after
|
|
938
|
+
* `decide`, so a replay whose `decide` fails against the advanced state
|
|
939
|
+
* reports that failure, and one whose `decide` returns no events succeeds as
|
|
940
|
+
* a no-op. Only a redelivery that appends fails with `DuplicateCommand`.
|
|
349
941
|
*/
|
|
350
942
|
readonly commandId?: string;
|
|
351
|
-
/**
|
|
352
|
-
|
|
943
|
+
/**
|
|
944
|
+
* The stream version the caller last saw — an HTTP `If-Match`.
|
|
945
|
+
*
|
|
946
|
+
* After state is loaded and **before `decide` runs**, a stream at any other
|
|
947
|
+
* version fails the call with `VersionConflict`, carrying the loaded version
|
|
948
|
+
* as `actualVersion`; `decide` never sees state the caller did not. Otherwise
|
|
949
|
+
* the append is conditioned on this version, so a writer that slips in
|
|
950
|
+
* between the load and the append also fails it with `VersionConflict`
|
|
951
|
+
* (without `actualVersion`). Neither is retried, whatever the handler's
|
|
952
|
+
* `retry` policy says.
|
|
953
|
+
*
|
|
954
|
+
* With `idempotency`, a mismatch is first checked against the command's
|
|
955
|
+
* sentinel (one strongly consistent `GetItem`, on the mismatch path only): a
|
|
956
|
+
* redelivery of a command that already committed — its response lost, the
|
|
957
|
+
* same `commandId` and If-Match sent again — fails with `DuplicateCommand`,
|
|
958
|
+
* not `VersionConflict`, matching the precedence `append` applies.
|
|
959
|
+
*
|
|
960
|
+
* A no-op decision (`decide` returns `[]`) at the matching version succeeds
|
|
961
|
+
* with the current state and version. Must be a non-negative integer — any
|
|
962
|
+
* other value fails with `ValidationError` before anything is read.
|
|
963
|
+
*/
|
|
964
|
+
readonly expectedVersion?: number | undefined;
|
|
965
|
+
/**
|
|
966
|
+
* Caller-owned transact items committed atomically with the produced events
|
|
967
|
+
* (see {@link AppendOptions.additionalItems}).
|
|
968
|
+
*
|
|
969
|
+
* Either a static array, or a function of the {@link Decision} — an inline
|
|
970
|
+
* projection — returning the array or an `Effect` of it. The function runs
|
|
971
|
+
* after `decide` and after the new events are folded, is not called when
|
|
972
|
+
* `decide` returns no events, and is re-run on every retry attempt. The items
|
|
973
|
+
* it returns count towards `AppendTooLarge`, and the `indices` of an
|
|
974
|
+
* `AdditionalItemConditionFailed` refer to the array it returned.
|
|
975
|
+
*/
|
|
976
|
+
readonly additionalItems?: AdditionalItemsInput<State, Event, E2, R2>;
|
|
353
977
|
}
|
|
354
978
|
/** Per-call options when the handler was created with `idempotency` — `commandId` is required. */
|
|
355
|
-
export interface IdempotentCommandOptions<TMetadata> extends CommandOptions<TMetadata> {
|
|
979
|
+
export interface IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never> extends CommandOptions<TMetadata, State, Event, E2, R2> {
|
|
356
980
|
readonly commandId: string;
|
|
357
981
|
}
|
|
358
982
|
/**
|
|
@@ -360,12 +984,28 @@ export interface IdempotentCommandOptions<TMetadata> extends CommandOptions<TMet
|
|
|
360
984
|
* required (and `commandId` within it non-optional), so a missing `commandId`
|
|
361
985
|
* is a compile error rather than a silent downgrade to at-least-once.
|
|
362
986
|
*/
|
|
363
|
-
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined> = TConfig extends {
|
|
987
|
+
type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2> = TConfig extends {
|
|
364
988
|
readonly idempotency: object;
|
|
365
|
-
} ? [options: IdempotentCommandOptions<TMetadata>] : [options?: CommandOptions<TMetadata> | undefined];
|
|
366
|
-
type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled;
|
|
367
|
-
|
|
368
|
-
|
|
989
|
+
} ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2> | undefined];
|
|
990
|
+
type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
|
|
991
|
+
/**
|
|
992
|
+
* A handler over an {@link EventStream}. Generic per call over the error (`E2`)
|
|
993
|
+
* and requirements (`R2`) of a function-form `additionalItems` that returns an
|
|
994
|
+
* `Effect`; both are `never` otherwise.
|
|
995
|
+
*/
|
|
996
|
+
type CommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2, DynamoClient | TableConfig | R2>;
|
|
997
|
+
/**
|
|
998
|
+
* A handler over a {@link BoundEventStream}: the stream's services are already
|
|
999
|
+
* provided, so a function-form `additionalItems`' `R2` is its only requirement.
|
|
1000
|
+
*/
|
|
1001
|
+
type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2, R2>;
|
|
1002
|
+
/**
|
|
1003
|
+
* The handler the data-last {@link commandHandler} returns for stream `S`: a
|
|
1004
|
+
* {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
|
|
1005
|
+
* stream is also structurally an `EventStream`), a {@link CommandHandler}
|
|
1006
|
+
* otherwise.
|
|
1007
|
+
*/
|
|
1008
|
+
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;
|
|
369
1009
|
/**
|
|
370
1010
|
* Create a command handler that reads, decides, and appends atomically.
|
|
371
1011
|
*
|
|
@@ -391,12 +1031,51 @@ type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends Read
|
|
|
391
1031
|
* idempotency: { ttl: Duration.days(1) },
|
|
392
1032
|
* })
|
|
393
1033
|
* yield* handle({ matchId: "m-1" }, command, { commandId: "cmd-7f3a" })
|
|
1034
|
+
*
|
|
1035
|
+
* // Load state from the inline snapshot alone (mode "inline" without `every`)
|
|
1036
|
+
* const handle = EventStore.commandHandler(decider, stream, { verifySnapshot: false })
|
|
1037
|
+
*
|
|
1038
|
+
* // If-Match: fail with VersionConflict (never retried) unless the stream is at v7
|
|
1039
|
+
* yield* handle({ matchId: "m-1" }, command, { expectedVersion: 7 })
|
|
1040
|
+
*
|
|
1041
|
+
* // Inline projection, committed in the same transaction as the events
|
|
1042
|
+
* yield* handle({ matchId: "m-1" }, command, {
|
|
1043
|
+
* additionalItems: ({ state }) => [Scoreboard.put({ matchId: "m-1", runs: state.runs })],
|
|
1044
|
+
* })
|
|
394
1045
|
* ```
|
|
395
1046
|
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
1047
|
+
* Each invocation runs
|
|
1048
|
+
* `load → [expectedVersion check] → decide → fold new events → derive items → append → [snapshot]`.
|
|
1049
|
+
*
|
|
1050
|
+
* - **Load.** Strongly consistent by default — see
|
|
1051
|
+
* {@link CommandHandlerOptions.consistentRead}. When the stream declares a
|
|
1052
|
+
* `snapshot` config, state is loaded with {@link EventStream.readLatest} —
|
|
1053
|
+
* the snapshot, the events after it and the head in one request in steady
|
|
1054
|
+
* state — and only those events are folded, instead of replaying the stream
|
|
1055
|
+
* from the beginning. On a `mode: "inline"` stream without `every`,
|
|
1056
|
+
* {@link CommandHandlerOptions.verifySnapshot}`: false` loads the snapshot
|
|
1057
|
+
* item alone (one `GetItem`); an answer that is not a successful append is
|
|
1058
|
+
* confirmed against the head first, and a snapshot that proves stale falls
|
|
1059
|
+
* back to the verified load — in which case `decide` may run twice.
|
|
1060
|
+
* - **expectedVersion.** See {@link CommandOptions.expectedVersion}.
|
|
1061
|
+
* - **Fold before append.** The new events are folded into state before the
|
|
1062
|
+
* append, so the state returned, snapshotted and handed to a function-form
|
|
1063
|
+
* `additionalItems` (see {@link Decision}) is always the `evolve` fold.
|
|
1064
|
+
* - **Snapshot.** With `snapshot.mode: "inline"`, the snapshot is written in
|
|
1065
|
+
* the append transaction itself — on every append, or at the `every`
|
|
1066
|
+
* cadence. With the default `"after-append"` and `snapshot.every` set, a
|
|
1067
|
+
* fresh snapshot is written (best-effort) after a successful append once the
|
|
1068
|
+
* cadence threshold is crossed. See {@link SnapshotMode}.
|
|
1069
|
+
* - **One atomic append.** One command, one decision, one append: a decision
|
|
1070
|
+
* too large for one transaction fails with `AppendTooLarge` before anything
|
|
1071
|
+
* is written. A large command is split by the application into stepped
|
|
1072
|
+
* commands — each an ordinary call, chained by `expectedVersion` — so a
|
|
1073
|
+
* failure partway leaves the stream at a real intermediate state.
|
|
1074
|
+
*
|
|
1075
|
+
* The handler is generic per call over the error and requirements of a
|
|
1076
|
+
* function-form `additionalItems` that returns an `Effect`: they join the
|
|
1077
|
+
* handler's error channel and requirements (on a `BoundEventStream` handler
|
|
1078
|
+
* they are its only requirements).
|
|
400
1079
|
*
|
|
401
1080
|
* Without `idempotency`, command processing is **at-least-once**: a retry after
|
|
402
1081
|
* an acked-but-lost response re-runs `decide` and appends again.
|
|
@@ -408,12 +1087,9 @@ type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends Read
|
|
|
408
1087
|
* `EventStreamTypeId` brand of the second argument.
|
|
409
1088
|
*/
|
|
410
1089
|
export declare const commandHandler: {
|
|
411
|
-
<State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig):
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
};
|
|
415
|
-
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
416
|
-
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
1090
|
+
<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> | EventStream<TEvent, ReadonlyArray<string>, any, State, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
|
|
1091
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
1092
|
+
<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
|
|
417
1093
|
};
|
|
418
1094
|
/**
|
|
419
1095
|
* Reconstruct state from events by folding through a decider's `evolve` function.
|