effect-dynamodb 1.24.0 → 1.26.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.
Files changed (106) hide show
  1. package/dist/Aggregate.d.ts +12 -2
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +284 -55
  4. package/dist/Aggregate.js.map +1 -1
  5. package/dist/Batch.d.ts +14 -5
  6. package/dist/Batch.d.ts.map +1 -1
  7. package/dist/Batch.js +184 -119
  8. package/dist/Batch.js.map +1 -1
  9. package/dist/Collection.d.ts +9 -1
  10. package/dist/Collection.d.ts.map +1 -1
  11. package/dist/Collection.js +5 -4
  12. package/dist/Collection.js.map +1 -1
  13. package/dist/DynamoClient.d.ts +80 -28
  14. package/dist/DynamoClient.d.ts.map +1 -1
  15. package/dist/DynamoClient.js +125 -25
  16. package/dist/DynamoClient.js.map +1 -1
  17. package/dist/Entity.d.ts +99 -40
  18. package/dist/Entity.d.ts.map +1 -1
  19. package/dist/Entity.js +693 -302
  20. package/dist/Entity.js.map +1 -1
  21. package/dist/EventStore.d.ts +598 -75
  22. package/dist/EventStore.d.ts.map +1 -1
  23. package/dist/EventStore.js +852 -221
  24. package/dist/EventStore.js.map +1 -1
  25. package/dist/Query.d.ts +100 -14
  26. package/dist/Query.d.ts.map +1 -1
  27. package/dist/Query.js +275 -71
  28. package/dist/Query.js.map +1 -1
  29. package/dist/StorageEncoding.d.ts +160 -0
  30. package/dist/StorageEncoding.d.ts.map +1 -0
  31. package/dist/StorageEncoding.js +258 -0
  32. package/dist/StorageEncoding.js.map +1 -0
  33. package/dist/Table.d.ts +56 -6
  34. package/dist/Table.d.ts.map +1 -1
  35. package/dist/Table.js +142 -60
  36. package/dist/Table.js.map +1 -1
  37. package/dist/Transaction.d.ts +8 -0
  38. package/dist/Transaction.d.ts.map +1 -1
  39. package/dist/Transaction.js +72 -22
  40. package/dist/Transaction.js.map +1 -1
  41. package/dist/VectorSearchEmulation.d.ts +3 -2
  42. package/dist/VectorSearchEmulation.d.ts.map +1 -1
  43. package/dist/VectorSearchEmulation.js +14 -2
  44. package/dist/VectorSearchEmulation.js.map +1 -1
  45. package/dist/compression/zlib.d.ts +75 -0
  46. package/dist/compression/zlib.d.ts.map +1 -0
  47. package/dist/compression/zlib.js +109 -0
  48. package/dist/compression/zlib.js.map +1 -0
  49. package/dist/index.d.ts +4 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/internal/BoundCrud.d.ts +39 -35
  54. package/dist/internal/BoundCrud.d.ts.map +1 -1
  55. package/dist/internal/BoundCrud.js.map +1 -1
  56. package/dist/internal/BoundQuery.d.ts +6 -5
  57. package/dist/internal/BoundQuery.d.ts.map +1 -1
  58. package/dist/internal/BoundQuery.js.map +1 -1
  59. package/dist/internal/BoundVectorQuery.d.ts +17 -1
  60. package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
  61. package/dist/internal/BoundVectorQuery.js +31 -6
  62. package/dist/internal/BoundVectorQuery.js.map +1 -1
  63. package/dist/internal/EntityCombinators.d.ts +8 -5
  64. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  65. package/dist/internal/EntityCombinators.js +24 -6
  66. package/dist/internal/EntityCombinators.js.map +1 -1
  67. package/dist/internal/EntityOps.d.ts +1 -1
  68. package/dist/internal/Expr.d.ts +9 -3
  69. package/dist/internal/Expr.d.ts.map +1 -1
  70. package/dist/internal/Expr.js +7 -5
  71. package/dist/internal/Expr.js.map +1 -1
  72. package/dist/internal/ExpressionLimits.d.ts +45 -0
  73. package/dist/internal/ExpressionLimits.d.ts.map +1 -0
  74. package/dist/internal/ExpressionLimits.js +89 -0
  75. package/dist/internal/ExpressionLimits.js.map +1 -0
  76. package/dist/internal/Hydrate.d.ts +62 -0
  77. package/dist/internal/Hydrate.d.ts.map +1 -0
  78. package/dist/internal/Hydrate.js +191 -0
  79. package/dist/internal/Hydrate.js.map +1 -0
  80. package/dist/internal/ItemSize.d.ts +60 -24
  81. package/dist/internal/ItemSize.d.ts.map +1 -1
  82. package/dist/internal/ItemSize.js +122 -39
  83. package/dist/internal/ItemSize.js.map +1 -1
  84. package/dist/internal/Packed.d.ts +289 -0
  85. package/dist/internal/Packed.d.ts.map +1 -0
  86. package/dist/internal/Packed.js +654 -0
  87. package/dist/internal/Packed.js.map +1 -0
  88. package/dist/internal/PathBuilder.d.ts +45 -2
  89. package/dist/internal/PathBuilder.d.ts.map +1 -1
  90. package/dist/internal/PathBuilder.js +4 -0
  91. package/dist/internal/PathBuilder.js.map +1 -1
  92. package/dist/internal/TransactWriteOps.d.ts +22 -46
  93. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  94. package/dist/internal/TransactWriteOps.js +26 -95
  95. package/dist/internal/TransactWriteOps.js.map +1 -1
  96. package/dist/internal/TransactableOps.d.ts +1 -1
  97. package/dist/internal/TransactableOps.d.ts.map +1 -1
  98. package/dist/internal/TransactableOps.js +2 -0
  99. package/dist/internal/TransactableOps.js.map +1 -1
  100. package/dist/internal/WriteDiagnosis.d.ts +353 -0
  101. package/dist/internal/WriteDiagnosis.d.ts.map +1 -0
  102. package/dist/internal/WriteDiagnosis.js +1028 -0
  103. package/dist/internal/WriteDiagnosis.js.map +1 -0
  104. package/dist/internal/sparse_check.js +43 -79
  105. package/dist/internal/sparse_check.js.map +1 -1
  106. package/package.json +6 -2
@@ -13,18 +13,26 @@
13
13
  * `VersionConflict` retry policy, caller-supplied `expectedVersion`,
14
14
  * decision-derived `additionalItems`, inline snapshots)
15
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
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
18
19
  * - `fold` / `foldFrom` helpers for state reconstruction
20
+ * - Packed storage (`makeStream({ storedAs: { data } })`, `snapshot.storedAs`):
21
+ * event data and snapshot state as one JSON string, optionally compressed —
22
+ * read by the stored form, so a stream may mix native, JSON and compressed
23
+ * items; `measureAppend` / `measureSnapshot` for caller-side size planning,
24
+ * and `decodeStreamImage` for DynamoDB Streams records
19
25
  *
20
26
  * Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
21
27
  * DynamoClient, Marshaller).
22
28
  */
23
29
  import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
30
+ import { type PackedConfig } from "@effect-dynamodb/schema/DynamoModel.js";
24
31
  import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
25
32
  import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
26
- import { type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
33
+ import { DateTime, type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
27
34
  import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
35
+ import { type StreamImage } from "./internal/Packed.js";
28
36
  import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
29
37
  import * as Query from "./Query.js";
30
38
  import { type Table, type TableConfig } from "./Table.js";
@@ -58,8 +66,35 @@ export interface StreamEvent<A, M = Record<string, unknown> | undefined> {
58
66
  readonly eventType: string;
59
67
  readonly data: A;
60
68
  readonly metadata: M;
69
+ /**
70
+ * ISO 8601 (UTC): when the event was appended, by the append's `Clock` — or
71
+ * the `eventTimestamps` entry it was appended with
72
+ * ({@link AppendOptionsBase.eventTimestamps}).
73
+ */
61
74
  readonly timestamp: string;
62
75
  }
76
+ /**
77
+ * A {@link StreamEvent} together with the event's stored `data` in its wire
78
+ * form — what `decodeStreamImage(image, { raw: true })` returns.
79
+ */
80
+ export interface RawStreamEvent<A, M = Record<string, unknown> | undefined> extends StreamEvent<A, M> {
81
+ /**
82
+ * The event's `data` as stored, before the event schema decoded it: the
83
+ * parsed JSON of a packed `data` (`{ _tag, ...fields }`), or the unmarshalled
84
+ * map of a native one — fields the event schema does not model included. The
85
+ * same object the decode read, so nothing is parsed twice.
86
+ */
87
+ readonly rawData: Readonly<Record<string, unknown>>;
88
+ }
89
+ /** Options of `decodeStreamImage`. */
90
+ export interface DecodeStreamImageOptions {
91
+ /**
92
+ * Also return the event's stored `data` in wire form (`rawData`, see
93
+ * {@link RawStreamEvent}) — for a consumer that needs fields the event
94
+ * schema does not model, without parsing the image a second time.
95
+ */
96
+ readonly raw?: boolean | undefined;
97
+ }
63
98
  /**
64
99
  * Metadata type carried on {@link StreamEvent} for a stream: the decoded
65
100
  * metadata schema type when the stream declares one, an untyped record
@@ -122,7 +157,69 @@ export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
122
157
  readonly schema: TSchema;
123
158
  readonly mode?: SnapshotMode | undefined;
124
159
  readonly every?: number | undefined;
160
+ /**
161
+ * How the snapshot's `state` is stored: a native map (omitted, the default)
162
+ * or packed — `DynamoModel.Packed({ format: "json", compression? })`, one
163
+ * JSON string, compressed to binary at or over the threshold when that is
164
+ * smaller. Packing shrinks a large state the most: an inline snapshot rides
165
+ * in every append's transaction, and DynamoDB's item and transaction size
166
+ * limits count its **stored** bytes.
167
+ *
168
+ * The state schema's encoded side must be object- or array-shaped
169
+ * (`[EDD-9073]`). Reads go by the stored form, so changing this setting
170
+ * needs no rewrite: a snapshot written before still decodes.
171
+ */
172
+ readonly storedAs?: PackedConfig | undefined;
125
173
  }
174
+ /**
175
+ * How a stream stores event `data` (`makeStream({ storedAs })`). `data` is a
176
+ * native map by default; `DynamoModel.Packed({ format: "json", compression? })`
177
+ * stores it as one JSON string (`S`), compressed to binary (`B`) at or over the
178
+ * threshold when that is smaller. Metadata always stays a map.
179
+ *
180
+ * Reads go by each item's stored form, never by this setting, so a stream may
181
+ * hold a mix — events appended before packing was turned on still decode, and
182
+ * so do packed events after it is turned off.
183
+ */
184
+ export interface StreamStorageConfig {
185
+ readonly data?: PackedConfig | undefined;
186
+ }
187
+ /** The resolved storage settings exposed on a stream (`stream.storedAs`). */
188
+ export interface StreamStorageSettings {
189
+ /** Event `data` (`makeStream({ storedAs: { data } })`); `undefined` = native map. */
190
+ readonly data: PackedConfig | undefined;
191
+ /** Snapshot `state` (`snapshot.storedAs`); `undefined` = native map. */
192
+ readonly state: PackedConfig | undefined;
193
+ }
194
+ /**
195
+ * The bytes an {@link EventStream.append} would write, by DynamoDB's own size
196
+ * accounting — {@link EventStream.measureAppend}. Every number counts stored
197
+ * bytes: packed (and compressed) `data` and `state` as they would be sent.
198
+ */
199
+ export interface AppendMeasurement {
200
+ /**
201
+ * The item size of each event `Put`, in order — what DynamoDB's 400 KB
202
+ * (409,600-byte) item limit measures.
203
+ */
204
+ readonly events: ReadonlyArray<number>;
205
+ /** The item size of the idempotency sentinel `Put`, when `idempotency` is given. */
206
+ readonly sentinel: number | undefined;
207
+ /** The item size of the inline snapshot `Put`, when `snapshot` is given. */
208
+ readonly snapshot: number | undefined;
209
+ /**
210
+ * What the stream's own actions (the contiguity check, the event puts, the
211
+ * sentinel and the inline snapshot) add to the `TransactWriteItems` request
212
+ * — its 4 MB (4,194,304-byte) limit counts items, keys and expressions.
213
+ * Caller `additionalItems` are not included: add their size yourself.
214
+ */
215
+ readonly transactionBytes: number;
216
+ }
217
+ /**
218
+ * The options {@link EventStream.measureAppend} accepts: those of
219
+ * {@link EventStream.append} except `additionalItems`.
220
+ */
221
+ export type MeasureAppendOptions<TMetadata, TState = never> = Omit<AppendOptionsBase<TState>, "additionalItems"> & AppendMetadataOptions<TMetadata>;
222
+ export type { LambdaAttributeValue, StreamImage } from "./internal/Packed.js";
126
223
  /**
127
224
  * How {@link commandHandler} writes snapshots.
128
225
  *
@@ -130,7 +227,7 @@ export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
130
227
  * succeeds, at the `every` cadence. A failed snapshot write never fails the
131
228
  * command; the snapshot simply lags until the next threshold crossing.
132
229
  * - `"inline"` — the snapshot `Put` rides in the append's own transaction (see
133
- * {@link AppendOptions.snapshot}), on every append, or at the `every` cadence
230
+ * {@link AppendOptionsBase.snapshot}), on every append, or at the `every` cadence
134
231
  * when one is set. The snapshot is then current after every command that
135
232
  * writes one, so {@link EventStream.readLatest} loads state in one request.
136
233
  * The snapshot counts towards the transaction's item and size limits.
@@ -153,7 +250,7 @@ export interface SnapshotSettings {
153
250
  * redelivery whose `decide` returns no events against the loaded state never
154
251
  * appends, so it succeeds as a no-op instead of failing with
155
252
  * `DuplicateCommand`. Whether a no-op redelivery needs to be told apart is the
156
- * application's call (see {@link CommandOptions.commandId}).
253
+ * application's call (see {@link CommandOptionsBase.commandId}).
157
254
  *
158
255
  * The sentinel is co-located in the stream's own partition, so `commandId`
159
256
  * uniqueness is scoped to the stream (which is what "have I already applied this
@@ -178,14 +275,46 @@ export interface AppendIdempotency {
178
275
  readonly ttl?: Duration.Duration | string;
179
276
  }
180
277
  /**
181
- * Options accepted by {@link EventStream.append}.
278
+ * The metadata of an {@link EventStream.append}: one value shared by every
279
+ * event (`metadata`), or one entry per event (`eventMetadata`) — never both.
280
+ *
281
+ * Passing both is a compile error, and a `ValidationError` at runtime for an
282
+ * untyped caller. `undefined` counts as absent for either, so
283
+ * `{ metadata: undefined, eventMetadata: [...] }` is not "both".
284
+ */
285
+ export type AppendMetadataOptions<TMetadata> = {
286
+ /**
287
+ * Metadata written onto **every** event of the append — a copy per item,
288
+ * encoded once through the stream's metadata schema when one is
289
+ * configured. For metadata that belongs to each event (an event id, the
290
+ * original occurrence time of an imported event) use `eventMetadata`:
291
+ * packing a list into `metadata` stores every entry on every item, so
292
+ * stored metadata grows with the square of the append size.
293
+ */
294
+ readonly metadata?: TMetadata;
295
+ readonly eventMetadata?: undefined;
296
+ } | {
297
+ readonly metadata?: undefined;
298
+ /**
299
+ * Per-event metadata: entry `i` is written onto event `i` (and read back
300
+ * as that event's `StreamEvent.metadata`). Each defined entry is encoded
301
+ * through the stream's metadata schema, as `metadata` is; an `undefined`
302
+ * entry writes its event without a `metadata` attribute.
303
+ *
304
+ * Must have exactly one entry per event — a length mismatch fails with
305
+ * `ValidationError` before anything is read or written, also for an
306
+ * append of no events (where only `[]` is accepted).
307
+ */
308
+ readonly eventMetadata?: ReadonlyArray<TMetadata | undefined>;
309
+ };
310
+ /**
311
+ * The options of {@link EventStream.append} other than its metadata (see
312
+ * {@link AppendMetadataOptions}).
182
313
  *
183
314
  * `TState` is the stream's snapshot state type (`never` for a stream without a
184
315
  * `snapshot` config, so `snapshot` cannot be supplied there).
185
316
  */
186
- export interface AppendOptions<TMetadata, TState = never> {
187
- /** Per-append metadata, validated against the stream's metadata schema when configured. */
188
- readonly metadata?: TMetadata;
317
+ export interface AppendOptionsBase<TState = never> {
189
318
  /**
190
319
  * Caller-owned transact items committed atomically with the events — the same
191
320
  * op union `Transaction.transactWrite` accepts (`EntityPut`, `EntityDelete`,
@@ -224,7 +353,34 @@ export interface AppendOptions<TMetadata, TState = never> {
224
353
  * is `never`) it dies with `[EDD-9026]`, as `writeSnapshot` does.
225
354
  */
226
355
  readonly snapshot?: TState;
356
+ /**
357
+ * Per-event timestamps: entry `i` is written as event `i`'s `timestamp` (and
358
+ * read back as that event's `StreamEvent.timestamp`), for history imported
359
+ * with its original occurrence times. An `undefined` entry keeps the
360
+ * append's `Clock` time. Independent of `metadata` / `eventMetadata`.
361
+ *
362
+ * Written with `DateTime.formatIso` — the format the append's own time is
363
+ * written in — so a zoned `DateTime` is stored as its UTC instant.
364
+ *
365
+ * - Must have exactly one entry per event, each a valid `DateTime` (finite
366
+ * epoch) or `undefined` — anything else fails with `ValidationError` before anything is read or
367
+ * written, also for an append of no events (where only `[]` is accepted).
368
+ * - Only the event items change: the idempotency sentinel, its TTL and the
369
+ * inline snapshot record when the append was written.
370
+ * - No ordering is enforced. `version` orders a stream; timestamps may
371
+ * repeat, run backwards or lie in the future, as imported history can.
372
+ */
373
+ readonly eventTimestamps?: ReadonlyArray<DateTime.DateTime | undefined>;
227
374
  }
375
+ /**
376
+ * Options accepted by {@link EventStream.append}: {@link AppendOptionsBase}
377
+ * plus the exclusive `metadata` / `eventMetadata` pair
378
+ * ({@link AppendMetadataOptions}).
379
+ *
380
+ * A type alias rather than an interface, since it is an intersection with a
381
+ * union: an interface cannot extend it.
382
+ */
383
+ export type AppendOptions<TMetadata, TState = never> = AppendOptionsBase<TState> & AppendMetadataOptions<TMetadata>;
228
384
  /** Error channel of {@link EventStream.append}. */
229
385
  export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
230
386
  /**
@@ -298,8 +454,10 @@ export interface LatestState<TState, TEvent, M = Record<string, unknown> | undef
298
454
  * - `undefined` leaves the event out of the index (a sparse index).
299
455
  * - An empty string (or any other value) fails the append with
300
456
  * `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.
457
+ * values — as does a `key` that throws. A key over DynamoDB's 1024-byte
458
+ * sort-key limit is sent and rejected by DynamoDB; the append then fails
459
+ * with `ValidationError` naming the index and the event (#144), and nothing
460
+ * is written.
303
461
  *
304
462
  * The key is derived only when an event is appended. Events are never
305
463
  * rewritten, so changing `key` later leaves events already stored under their
@@ -322,9 +480,9 @@ export type StreamIndexKey<TEvent> = (event: TEvent, version: number) => string
322
480
  * An LSI must be created with the table. A table with any LSI caps the item
323
481
  * collection of **every** partition key value in it at 10 GB — not only the
324
482
  * 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}.
483
+ * entity partition sharing the table; a write that would exceed it fails. What
484
+ * the index copies from each event is its `projection` — see
485
+ * {@link StreamIndexProjection}.
328
486
  */
329
487
  export interface LocalStreamIndexConfig<TEvent> {
330
488
  readonly type?: "lsi" | undefined;
@@ -333,13 +491,19 @@ export interface LocalStreamIndexConfig<TEvent> {
333
491
  /** An LSI uses the table partition key; supplying `pk` is refused (`[EDD-9063]`). */
334
492
  readonly pk?: undefined;
335
493
  readonly key: StreamIndexKey<TEvent>;
494
+ /**
495
+ * What the index copies from each event — see {@link StreamIndexProjection}.
496
+ * Default `"all"`. Must match the deployed index.
497
+ */
498
+ readonly projection?: StreamIndexProjection | undefined;
336
499
  }
337
500
  /**
338
501
  * A stream index on a **global** secondary index. `append` writes the stream's
339
502
  * partition key value into the `pk` attribute of each indexed event, so the
340
503
  * 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`.
504
+ * an LSI, without the LSI's 10 GB item-collection cap. DynamoDB rejects a
505
+ * strongly consistent read of it, diagnosed as a `ValidationError` naming the
506
+ * index.
343
507
  *
344
508
  * Unlike an LSI, the GSI itself can be added to an existing table
345
509
  * (`UpdateTable`), but that does not index the stream's existing events: the
@@ -350,6 +514,7 @@ export interface LocalStreamIndexConfig<TEvent> {
350
514
  * - `pk` — the attribute carrying the stream's partition key (the GSI's HASH key).
351
515
  * - `sk` — the attribute carrying the derived key (the GSI's RANGE key).
352
516
  * - `key` — see {@link StreamIndexKey}.
517
+ * - `projection` — see {@link StreamIndexProjection}.
353
518
  */
354
519
  export interface GlobalStreamIndexConfig<TEvent> {
355
520
  readonly type: "gsi";
@@ -357,6 +522,11 @@ export interface GlobalStreamIndexConfig<TEvent> {
357
522
  readonly pk: string;
358
523
  readonly sk: string;
359
524
  readonly key: StreamIndexKey<TEvent>;
525
+ /**
526
+ * What the index copies from each event — see {@link StreamIndexProjection}.
527
+ * Default `"all"`. Must match the deployed index.
528
+ */
529
+ readonly projection?: StreamIndexProjection | undefined;
360
530
  }
361
531
  /**
362
532
  * One entry of `makeStream({ indexes })`: a sub-stream of the stream's events,
@@ -364,30 +534,95 @@ export interface GlobalStreamIndexConfig<TEvent> {
364
534
  * and {@link GlobalStreamIndexConfig}.
365
535
  */
366
536
  export type StreamIndexConfig<TEvent> = LocalStreamIndexConfig<TEvent> | GlobalStreamIndexConfig<TEvent>;
537
+ /**
538
+ * What a stream index copies from each event (`projection` on an entry of
539
+ * `makeStream({ indexes })`). {@link indexDefinitions} emits the matching
540
+ * `Projection`, and the reads choose their path from it.
541
+ *
542
+ * | Value | Emitted `Projection` | Read |
543
+ * |---|---|---|
544
+ * | `"all"` (default) | `ALL` | one query |
545
+ * | `"withoutMetadata"` | `INCLUDE` of every event attribute except `metadata` | one query; `metadata` is `undefined` (and typed so) |
546
+ * | `"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 |
547
+ *
548
+ * `__edd_e__` is always projected because every read filters on it, and
549
+ * DynamoDB refuses a filter on an attribute an index does not project.
550
+ *
551
+ * `metadata` is often an event's largest attribute; `"withoutMetadata"` keeps
552
+ * it out of the index, and `"keys"` keeps the index entries minimal at the cost
553
+ * of a table read per event read (rounded up to 4 KB each).
554
+ *
555
+ * **The declared projection must match the deployed index.** The library does
556
+ * not migrate: an LSI's projection is fixed when the table is created, and a
557
+ * GSI's cannot be updated (it must be deleted and created again). Tables
558
+ * deployed with CDK or Terraform copy `Projection` from
559
+ * {@link indexDefinitions}' output. An index deployed narrower than declared
560
+ * fails reads: DynamoDB refuses the `__edd_e__` filter ("does not project one
561
+ * or more filter attributes"), or rows lack event attributes, which the read
562
+ * reports as a `ValidationError` (`operation: "EventStore.index.decode"`)
563
+ * naming the index.
564
+ */
565
+ export type StreamIndexProjection = "all" | "withoutMetadata" | "keys";
367
566
  /**
368
567
  * The runtime settings of one stream index, as exposed on
369
568
  * {@link EventStream.indexes}. `pk` is the index's partition key attribute:
370
569
  * the table's `pk` for an LSI, the configured attribute for a GSI.
570
+ * `projection` is resolved: `"all"` when the index declares none.
371
571
  */
372
572
  export interface StreamIndexSettings {
373
573
  readonly type: "lsi" | "gsi";
374
574
  readonly index: string;
375
575
  readonly pk: string;
376
576
  readonly sk: string;
577
+ readonly projection: StreamIndexProjection;
377
578
  }
579
+ /**
580
+ * The `metadata` type of an event read through a stream index of projection
581
+ * `P`: `undefined` for a `"withoutMetadata"` index, which does not hold it,
582
+ * the stream's {@link StreamMetadata} otherwise.
583
+ */
584
+ export type IndexEventMetadata<TMetadata, P extends StreamIndexProjection> = [P] extends [
585
+ "withoutMetadata"
586
+ ] ? undefined : StreamMetadata<TMetadata>;
587
+ /** @internal The `projection` an index config declares (`"all"` when none). */
588
+ type ProjectionOf<C> = C extends {
589
+ readonly projection: infer P extends StreamIndexProjection;
590
+ } ? P : "all";
591
+ /**
592
+ * The projection of each index of a `makeStream({ indexes })` config, by name.
593
+ * `"all"` for an index that declares none.
594
+ */
595
+ export type IndexProjectionsOf<TIndexes> = {
596
+ readonly [K in keyof TIndexes & string]: ProjectionOf<TIndexes[K]>;
597
+ };
598
+ /** @internal The members a stream index entry may declare. */
599
+ type StreamIndexConfigKey = keyof LocalStreamIndexConfig<unknown> | keyof GlobalStreamIndexConfig<unknown>;
600
+ /**
601
+ * The `indexes` parameter of {@link makeStream}: each entry is a
602
+ * {@link StreamIndexConfig}, and a member it does not declare (a misspelled
603
+ * `projection`, say) is typed `never`, so it is a compile error. Inferring
604
+ * `TIndexes` from the entries turns off TypeScript's own excess-property check,
605
+ * which would otherwise catch it.
606
+ */
607
+ export type StreamIndexesConfig<TIndexes, TEvent> = {
608
+ readonly [K in keyof TIndexes]: StreamIndexConfig<TEvent> & {
609
+ readonly [P in Exclude<keyof TIndexes[K], StreamIndexConfigKey>]: never;
610
+ };
611
+ };
378
612
  /**
379
613
  * Options accepted by {@link EventStream.readIndex}.
380
614
  *
381
615
  * - `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.
616
+ * one of them, string bounds). Omitted, the whole sub-stream is read.
617
+ * DynamoDB rejects an empty bound, and a `between` whose lower bound sorts
618
+ * (by UTF-8 bytes) after its upper bound; the read then fails with a
619
+ * `ValidationError` naming the index and the bound, DynamoDB's error as its
620
+ * cause.
386
621
  * - `reverse` — descending key order.
387
622
  * - `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.
623
+ * - `consistentRead` — strongly consistent read. DynamoDB rejects it on a
624
+ * GSI-backed index, which it reads only eventually consistently: the read
625
+ * fails with a `ValidationError` naming the index.
391
626
  */
392
627
  export type ReadIndexOptions = {
393
628
  readonly reverse?: boolean | undefined;
@@ -413,9 +648,15 @@ export type EventStreamTypeId = typeof EventStreamTypeId;
413
648
  *
414
649
  * `TIndexName` is the union of the stream's index names (`makeStream({ indexes })`),
415
650
  * `never` for a stream without indexes, so {@link EventStream.readIndex} and
416
- * `query.index` only accept a declared name.
651
+ * `query.index` only accept a declared name. `TIndexProjections` is each
652
+ * index's {@link StreamIndexProjection} by name, so an event read through a
653
+ * `"withoutMetadata"` index has `metadata: undefined`.
417
654
  */
418
- export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
655
+ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
656
+ readonly [K in TIndexName]: StreamIndexProjection;
657
+ } = {
658
+ readonly [K in TIndexName]: "all";
659
+ }> extends Pipeable.Pipeable {
419
660
  readonly [EventStreamTypeId]: EventStreamTypeId;
420
661
  readonly streamName: string;
421
662
  readonly eventSchema: Schema.Top;
@@ -434,6 +675,11 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
434
675
  * snapshots.
435
676
  */
436
677
  readonly snapshotConfig: SnapshotSettings | undefined;
678
+ /**
679
+ * How the stream stores event `data` and snapshot `state` — see
680
+ * {@link StreamStorageConfig} and {@link SnapshotConfig.storedAs}.
681
+ */
682
+ readonly storedAs: StreamStorageSettings;
437
683
  /**
438
684
  * Write (or overwrite) this stream's snapshot.
439
685
  *
@@ -515,22 +761,92 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
515
761
  * Read the stream's events in the order of one of its indexes (#140) — the
516
762
  * events whose index `key` returned a string, ordered by that key. A
517
763
  * 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}.
764
+ * the key range, `reverse`, `limit` and `consistentRead` (which DynamoDB
765
+ * rejects on a GSI; the error names the index) — see {@link ReadIndexOptions}.
766
+ *
767
+ * How the events are read follows the index's {@link StreamIndexProjection}:
768
+ * one query for `"all"` and `"withoutMetadata"` (whose events have
769
+ * `metadata: undefined`, as the type says); for `"keys"`, one query with
770
+ * `Select: ALL_ATTRIBUTES` on an LSI, or the query followed by `BatchGetItem`
771
+ * (unprocessed keys retried, index order kept) on a GSI.
772
+ */
773
+ 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>;
774
+ /**
775
+ * The bytes {@link EventStream.append} would write for these arguments, by
776
+ * DynamoDB's own size accounting — for planning chunks against the 400 KB
777
+ * item and 4 MB transaction limits without re-implementing the stream's
778
+ * item layout or packing. The items are built by the same code `append`
779
+ * uses (envelope, keys, packed and compressed `data`, metadata, stream-index
780
+ * attributes, the sentinel, the inline snapshot), so the numbers are exact
781
+ * for the request `append` would send now with this runtime's compressors.
782
+ *
783
+ * Nothing is read or written, and nothing is refused for its size: this is
784
+ * caller-side planning, not a pre-check. `additionalItems` are not accepted
785
+ * — add their size yourself. Fails as `append` would on invalid options
786
+ * (`ValidationError`), and — with a `CompressorNotRegistered` cause — when a
787
+ * value crosses its compression threshold without a registered compressor.
788
+ */
789
+ measureAppend(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: MeasureAppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendMeasurement, ValidationError, TableConfig>;
790
+ /**
791
+ * The item size, in bytes, of the snapshot {@link EventStream.writeSnapshot}
792
+ * (or an inline snapshot at `asOfVersion`) would write — packed `state` as
793
+ * stored. Caller-side planning, like {@link EventStream.measureAppend}. Dies
794
+ * with `[EDD-9026]` on a stream without a `snapshot` config.
520
795
  */
521
- readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
796
+ measureSnapshot(streamId: StreamIdInput<TStreamIdFields>, state: TState, asOfVersion: number): Effect.Effect<number, ValidationError, TableConfig>;
797
+ /**
798
+ * Decode a DynamoDB Streams record image (`NewImage` / `OldImage`) of one
799
+ * of this stream's events into exactly the {@link StreamEvent} the read
800
+ * paths return — `data` decoded through the event schemas (by its stored
801
+ * form: native map, JSON string or compressed binary), `metadata` through
802
+ * the metadata schema when the stream declares one.
803
+ *
804
+ * Accepts DynamoDB JSON as Lambda and EventBridge Pipes deliver it (`B` /
805
+ * `BS` members as base64 text, or as bytes), or an already-unmarshalled
806
+ * item (a packed `data` as bytes or base64 text). `Option.none()` for an
807
+ * image that is not one of this stream's events — its snapshot, an
808
+ * idempotency sentinel, another stream's or an entity's item — so one
809
+ * handler can fan a table's records across decoders. The type is read off
810
+ * the raw image before anything is unmarshalled, so nothing in another
811
+ * item's image (a number the SDK cannot represent, say) fails the decode.
812
+ * An event image that does not decode fails with `ValidationError`.
813
+ *
814
+ * With `{ raw: true }`, each event also carries `rawData`: its `data` as
815
+ * stored, in wire form, unmodelled fields included (see
816
+ * {@link RawStreamEvent}).
817
+ *
818
+ * The compressor registry is not in the type (`R = never`): this unbound
819
+ * method reads the `Compression` service from the running fiber. When the
820
+ * stream's `storedAs.data` or `snapshot.storedAs` names a codec that
821
+ * registry does not hold — including no `Compression` at all, for example
822
+ * one lost behind `Layer.provide` — it dies with the `[EDD-9078]`
823
+ * configuration defect on first use, whatever the image, rather than
824
+ * failing with a typed error. Provide `Compression` where the handler runs,
825
+ * or decode through {@link EventStore.bind}, which captures it.
826
+ */
827
+ decodeStreamImage(image: StreamImage, options: DecodeStreamImageOptions & {
828
+ readonly raw: true;
829
+ }): Effect.Effect<Option.Option<RawStreamEvent<TEvent, StreamMetadata<TMetadata>>>, ValidationError, never>;
830
+ decodeStreamImage(image: StreamImage, options?: DecodeStreamImageOptions): Effect.Effect<Option.Option<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, ValidationError, never>;
522
831
  readonly query: {
523
832
  events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
524
833
  /**
525
834
  * A query of one of the stream's indexes (#140), scoped to `streamId`:
526
835
  * decoded events in index-key order. It composes with the `Query`
527
836
  * 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
837
+ * `Query.reverse`, `Query.limit`, `Query.consistentRead` (DynamoDB rejects
838
+ * it on a GSI-backed index: a `ValidationError` naming the index) — and terminals
530
839
  * (`Query.collect`, `Query.execute`, `Query.paginate`). The
531
840
  * `__edd_e__ = <stream>.event` filter still applies.
841
+ *
842
+ * It reads as {@link EventStream.readIndex} does for the index's
843
+ * projection: a `"keys"` GSI fetches each page's events with
844
+ * `BatchGetItem` (`Query.select` moves onto that fetch); `Query.count`
845
+ * counts index entries and fetches nothing. On a `"keys"` or
846
+ * `"withoutMetadata"` index, a `Query.filter` on an attribute the index
847
+ * does not project is refused by DynamoDB — use `Query.filterBy`.
532
848
  */
533
- index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
849
+ index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
534
850
  };
535
851
  }
536
852
  /**
@@ -592,7 +908,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
592
908
  * event's derived key (and, for a GSI, the stream's partition key) on the
593
909
  * event item; snapshots and idempotency sentinels never carry them, so they
594
910
  * are never in an index. Read with {@link EventStream.readIndex} or
595
- * `query.index`, and create the indexes with {@link indexDefinitions}.
911
+ * `query.index`, and create the indexes with {@link indexDefinitions}. Each
912
+ * index's `projection` (default `"all"`) sets what it copies from each event
913
+ * and how it is read — see {@link StreamIndexProjection}.
596
914
  *
597
915
  * Index attributes are written only at append time and events are never
598
916
  * rewritten. Declaring an index on a stream that already holds events, or
@@ -629,7 +947,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
629
947
  * `"inline"`.
630
948
  * @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
631
949
  * `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, an empty `index` / `sk`
632
- * / `pk`, or a `key` that is not a function.
950
+ * / `pk`, a `key` that is not a function, a `projection` other than
951
+ * `"all"` / `"withoutMetadata"` / `"keys"`, or a member an index entry does
952
+ * not declare (such as a misspelled `projection`, also a type error).
633
953
  * @throws `[EDD-9064]` when an index attribute is one the stream writes itself
634
954
  * (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
635
955
  * `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
@@ -638,10 +958,22 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
638
958
  * with `ValidationError`.
639
959
  * @throws `[EDD-9065]` when two indexes share a physical index name or an
640
960
  * attribute (or a GSI's `pk` and `sk` are the same attribute).
961
+ * @throws `[EDD-9079]` when `storedAs` declares a key other than `data`
962
+ * (metadata always stays a map), or a `storedAs.data` / `snapshot.storedAs`
963
+ * value is not a `DynamoModel.Packed(...)` config.
964
+ * @throws `[EDD-9073]` when `snapshot.storedAs` is set and the state schema's
965
+ * encoded side is not object- or array-shaped.
966
+ *
967
+ * `storedAs: { data: DynamoModel.Packed(...) }` stores event `data` packed, and
968
+ * `snapshot.storedAs` the snapshot `state` — see {@link StreamStorageConfig}.
969
+ * Register the codecs a compressed config names through the `Compression`
970
+ * service (`Compression.layer([...])` merged into the application layer): a
971
+ * bound stream refuses to bind without them, and an unbound one to run its
972
+ * first operation (`[EDD-9078]`).
641
973
  */
642
974
  export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top>, TTable extends Table, const TStreamName extends string, const TStreamId extends {
643
975
  readonly composite: ReadonlyArray<string>;
644
- }, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexName extends string = never>(config: {
976
+ }, 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: {
645
977
  readonly table: TTable;
646
978
  readonly streamName: TStreamName;
647
979
  readonly events: TEvents;
@@ -649,8 +981,10 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
649
981
  readonly metadata?: TMetadata;
650
982
  readonly snapshot?: TSnapshot;
651
983
  readonly casing?: DynamoSchema.Casing | undefined;
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>;
984
+ readonly indexes?: TIndexes & StreamIndexesConfig<TIndexes, Schema.Schema.Type<TEvents[number]>>;
985
+ /** How event `data` is stored — see {@link StreamStorageConfig}. */
986
+ readonly storedAs?: StreamStorageConfig | undefined;
987
+ }) => 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>>;
654
988
  /**
655
989
  * An EventStream whose operations have `DynamoClient` and `TableConfig` already
656
990
  * resolved, so all methods return `Effect<A, E, never>`.
@@ -671,12 +1005,18 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
671
1005
  * }) {}
672
1006
  * ```
673
1007
  */
674
- export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
1008
+ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
1009
+ readonly [K in TIndexName]: StreamIndexProjection;
1010
+ } = {
1011
+ readonly [K in TIndexName]: "all";
1012
+ }> extends Pipeable.Pipeable {
675
1013
  readonly [EventStreamTypeId]: EventStreamTypeId;
676
1014
  readonly streamName: string;
677
1015
  readonly eventSchema: Schema.Top;
678
1016
  /** See {@link EventStream.snapshotConfig}. */
679
1017
  readonly snapshotConfig: SnapshotSettings | undefined;
1018
+ /** See {@link EventStream.storedAs}. */
1019
+ readonly storedAs: StreamStorageSettings;
680
1020
  /** See {@link EventStream.indexes}. */
681
1021
  readonly indexes: {
682
1022
  readonly [K in TIndexName]: StreamIndexSettings;
@@ -696,15 +1036,27 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
696
1036
  /** See {@link EventStream.readLatest}. */
697
1037
  readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, never>;
698
1038
  /** See {@link EventStream.readIndex}. */
699
- readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
1039
+ readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, never>;
1040
+ /** See {@link EventStream.measureAppend}. */
1041
+ measureAppend(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: MeasureAppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendMeasurement, ValidationError, never>;
1042
+ /** See {@link EventStream.measureSnapshot}. */
1043
+ measureSnapshot(streamId: StreamIdInput<TStreamIdFields>, state: TState, asOfVersion: number): Effect.Effect<number, ValidationError, never>;
1044
+ /** See {@link EventStream.decodeStreamImage}. */
1045
+ decodeStreamImage(image: StreamImage, options: DecodeStreamImageOptions & {
1046
+ readonly raw: true;
1047
+ }): Effect.Effect<Option.Option<RawStreamEvent<TEvent, StreamMetadata<TMetadata>>>, ValidationError, never>;
1048
+ decodeStreamImage(image: StreamImage, options?: DecodeStreamImageOptions): Effect.Effect<Option.Option<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, ValidationError, never>;
700
1049
  /**
701
1050
  * The stream's query builders — unbound: run them with {@link provide}, or
702
- * where `DynamoClient` and `TableConfig` are provided.
1051
+ * where `DynamoClient` and `TableConfig` are provided. They do not use the
1052
+ * context this bound stream captured unless run through {@link provide}:
1053
+ * run directly, raw-row unpacking (`Query.select`) and decoding read the
1054
+ * `Compression` registry of the caller's context, not the bound one.
703
1055
  */
704
1056
  readonly query: {
705
1057
  events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
706
1058
  /** See `EventStream.query.index`. */
707
- index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
1059
+ index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
708
1060
  };
709
1061
  /** Escape hatch: provide DynamoClient | TableConfig to an arbitrary effect. */
710
1062
  readonly provide: <A, E>(effect: Effect.Effect<A, E, DynamoClient | TableConfig>) => Effect.Effect<A, E, never>;
@@ -723,7 +1075,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
723
1075
  * yield* stream.append({ matchId: "m-1" }, [event], 0) // R = never
724
1076
  * ```
725
1077
  */
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>;
1078
+ 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>;
727
1079
  /**
728
1080
  * `CreateTable` fragments for the stream indexes of one or more streams — see
729
1081
  * {@link indexDefinitions}. The lists are absent when empty, because
@@ -738,11 +1090,26 @@ export interface StreamIndexDefinitions {
738
1090
  /**
739
1091
  * Derive the `CreateTable` fragments for the stream indexes (#140) of the given
740
1092
  * 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.
1093
+ * `LocalSecondaryIndexes` and `GlobalSecondaryIndexes` — to merge into the
1094
+ * caller's `CreateTable` input. Event tables are not derived by
1095
+ * `Table.definition`, and the table's own `pk` / `sk` are not included: add
1096
+ * them (and, on a provisioned table, each GSI's `ProvisionedThroughput`)
1097
+ * yourself.
1098
+ *
1099
+ * Each index's `Projection` follows its {@link StreamIndexProjection}:
1100
+ *
1101
+ * | `projection` | `Projection` |
1102
+ * |---|---|
1103
+ * | `"all"` (default) | `{ ProjectionType: "ALL" }` |
1104
+ * | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
1105
+ * | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
1106
+ *
1107
+ * The deployed index must project what is declared — copy `Projection` from
1108
+ * this output into CDK or Terraform definitions. Neither an LSI's nor a GSI's
1109
+ * projection can be changed in place, and the library migrates nothing. A
1110
+ * table projects at most 100 `NonKeyAttributes` across all its indexes;
1111
+ * `"withoutMetadata"` spends 6 of them and `"keys"` 1 (DynamoDB refuses a
1112
+ * table over the limit).
746
1113
  *
747
1114
  * An index shared by several streams with the same definition is emitted
748
1115
  * once. Lists are sorted by index name, attributes by attribute name.
@@ -750,7 +1117,6 @@ export interface StreamIndexDefinitions {
750
1117
  * - An LSI can only be created with the table, and caps the item collection
751
1118
  * of every partition key value in the table at 10 GB — entity partitions
752
1119
  * and streams without indexes included.
753
- * - The projection must stay `ALL`: events are decoded from the index item.
754
1120
  *
755
1121
  * @example
756
1122
  * ```typescript
@@ -773,12 +1139,15 @@ export interface StreamIndexDefinitions {
773
1139
  * ```
774
1140
  *
775
1141
  * @throws `[EDD-9067]` when two streams define the same physical index
776
- * differently (another type, or other key attributes).
1142
+ * differently (another type, other key attributes, or another projection).
777
1143
  */
778
1144
  export declare const indexDefinitions: (...streams: ReadonlyArray<{
779
1145
  readonly streamName: string;
780
1146
  readonly indexes: {
781
- readonly [name: string]: StreamIndexSettings;
1147
+ readonly [name: string]: Omit<StreamIndexSettings, "projection"> & {
1148
+ /** Read as `"all"` when absent (a hand-built settings object). */
1149
+ readonly projection?: StreamIndexProjection | undefined;
1150
+ };
782
1151
  };
783
1152
  }>) => StreamIndexDefinitions;
784
1153
  /**
@@ -803,7 +1172,7 @@ export interface CommandHandlerOptions {
803
1172
  * initial attempt). Omit for the default: no retry.
804
1173
  *
805
1174
  * `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
806
- * any call that supplies {@link CommandOptions.expectedVersion}: the caller asked
1175
+ * any call that supplies {@link CommandOptionsBase.expectedVersion}: the caller asked
807
1176
  * for a conditional write, so its `VersionConflict` is always surfaced.
808
1177
  */
809
1178
  readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
@@ -850,7 +1219,7 @@ export interface CommandHandlerOptions {
850
1219
  *
851
1220
  * - **No snapshot item** (a stream not yet snapshotted): the verified load
852
1221
  * runs at once, and `decide` runs once.
853
- * - **With {@link CommandOptions.expectedVersion}**: a snapshot whose
1222
+ * - **With {@link CommandOptionsBase.expectedVersion}**: a snapshot whose
854
1223
  * `asOfVersion` differs from it is re-read verified before the If-Match
855
1224
  * check, so only a genuine mismatch is reported (its `actualVersion` is
856
1225
  * the verified head). A snapshot at the expected version holds exactly the
@@ -890,7 +1259,9 @@ export interface CommandHandlerOptions {
890
1259
  }
891
1260
  /**
892
1261
  * The outcome of one `decide` call, handed to a function-form
893
- * {@link CommandOptions.additionalItems}.
1262
+ * {@link CommandOptionsBase.additionalItems}, to `eventMetadata`
1263
+ * ({@link CommandMetadataOptions}) and to
1264
+ * {@link CommandOptionsBase.eventTimestamps}.
894
1265
  *
895
1266
  * - `events` — what `decide` returned (never empty: the function is not called
896
1267
  * for a no-op decision).
@@ -911,7 +1282,7 @@ export interface Decision<State, Event> {
911
1282
  readonly version: number;
912
1283
  }
913
1284
  /**
914
- * The forms {@link CommandOptions.additionalItems} accepts: a static array of
1285
+ * The forms {@link CommandOptionsBase.additionalItems} accepts: a static array of
915
1286
  * transact ops, or a function of the {@link Decision} returning the array
916
1287
  * directly (pure projection) or as an `Effect` (a projection that needs a read).
917
1288
  * The effect's `E2` joins the handler's error channel and its `R2` the handler's
@@ -919,15 +1290,97 @@ export interface Decision<State, Event> {
919
1290
  */
920
1291
  export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
921
1292
  /**
922
- * Per-call options accepted by a handler produced by {@link commandHandler}.
1293
+ * The form `eventMetadata` ({@link CommandMetadataOptions}) accepts: a
1294
+ * function of the {@link Decision} returning one metadata entry per event
1295
+ * (`undefined` for an event without metadata), directly or as an `Effect`. The
1296
+ * effect's `E3` joins the handler's error channel and its `R3` the handler's
1297
+ * requirements.
923
1298
  *
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.
1299
+ * Function-only: the number of events is known only once `decide` has run.
1300
+ * The `Effect` form lets ids be minted through the `Crypto` service
1301
+ * (`crypto.randomULID`, or `randomUUIDv7`), which keeps them deterministic
1302
+ * under `TestClock` and a test `Crypto` layer with fixed bytes.
928
1303
  */
929
- export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never> {
1304
+ export type EventMetadataInput<State, Event, TMetadata, E3 = never, R3 = never> = (decision: Decision<State, Event>) => ReadonlyArray<TMetadata | undefined> | Effect.Effect<ReadonlyArray<TMetadata | undefined>, E3, R3>;
1305
+ /**
1306
+ * The form {@link CommandOptionsBase.eventTimestamps} accepts: a function of
1307
+ * the {@link Decision} returning one timestamp per event (`undefined` keeps
1308
+ * the append's `Clock` time), directly or as an `Effect` — exactly as
1309
+ * {@link EventMetadataInput}. The effect's `E4` joins the handler's error
1310
+ * channel and its `R4` the handler's requirements.
1311
+ *
1312
+ * Function-only: the number of events is known only once `decide` has run.
1313
+ * The `Effect` form lets times come from a service (a source log, a
1314
+ * per-tenant clock) or from `Clock`, which keeps them deterministic under
1315
+ * `TestClock`.
1316
+ *
1317
+ * `E4` / `R4` are separate from `eventMetadata`'s `E3` / `R3`: with one shared
1318
+ * pair, an `eventMetadata` effect and an `eventTimestamps` effect with
1319
+ * different error types would not compile — TypeScript fixes the parameter
1320
+ * from one candidate and rejects the other instead of inferring a union.
1321
+ */
1322
+ 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>;
1323
+ /**
1324
+ * The per-call metadata of a {@link commandHandler} handler: one value shared
1325
+ * by every event (`metadata`), or a function deriving one entry per event from
1326
+ * the decision (`eventMetadata`) — never both (see
1327
+ * {@link AppendMetadataOptions}).
1328
+ */
1329
+ export type CommandMetadataOptions<TMetadata, State, Event, E3 = never, R3 = never> = {
1330
+ /**
1331
+ * Metadata written onto every event the decision appends (see
1332
+ * {@link AppendMetadataOptions}). Fixed before `decide` runs; to give
1333
+ * each event its own entry, use `eventMetadata`.
1334
+ */
930
1335
  readonly metadata?: TMetadata;
1336
+ readonly eventMetadata?: undefined;
1337
+ } | {
1338
+ readonly metadata?: undefined;
1339
+ /**
1340
+ * Per-event metadata derived from the {@link Decision}: the function
1341
+ * returns one entry per event, in order, and entry `i` is written onto
1342
+ * event `i` (see {@link AppendMetadataOptions}).
1343
+ *
1344
+ * - Runs after `decide` and after the new events are folded, receiving
1345
+ * the same `Decision` a function-form `additionalItems` does — and
1346
+ * **before** `additionalItems`, so an effectful pair runs in a
1347
+ * predictable order.
1348
+ * - Not called when `decide` returns no events.
1349
+ * - Re-run on every attempt: each `VersionConflict` retry, and the
1350
+ * re-decision a stale unverified snapshot triggers
1351
+ * ({@link CommandHandlerOptions.verifySnapshot}`: false`). Ids minted
1352
+ * per attempt are safe — a failed attempt writes nothing.
1353
+ * - A result whose length differs from the decision's events fails with
1354
+ * `ValidationError` (`operation: "EventStore.commandHandler.eventMetadata"`),
1355
+ * never retried. A failure of the returned `Effect` surfaces as is,
1356
+ * and is not retried either.
1357
+ *
1358
+ * Without `idempotency`, a redelivery after a lost response decides and
1359
+ * appends again, minting new ids. With it, the redelivery fails with
1360
+ * `DuplicateCommand`, so the first commit's ids are the only ones written:
1361
+ *
1362
+ * ```ts
1363
+ * // `crypto` is the `Crypto.Crypto` service; a ULID takes its time from the
1364
+ * // `Clock`, so `TestClock` plus a fixed-bytes `Crypto` make it deterministic
1365
+ * const handle = EventStore.commandHandler(decider, stream, {
1366
+ * idempotency: { ttl: Duration.days(1) },
1367
+ * })
1368
+ * yield* handle(streamId, command, {
1369
+ * commandId: deliveryId,
1370
+ * eventMetadata: ({ events }) =>
1371
+ * Effect.forEach(events, () =>
1372
+ * Effect.map(crypto.randomULID, (eventId) => ({ eventId })),
1373
+ * ),
1374
+ * })
1375
+ * ```
1376
+ */
1377
+ readonly eventMetadata?: EventMetadataInput<State, Event, TMetadata, E3, R3>;
1378
+ };
1379
+ /**
1380
+ * The options of a {@link commandHandler} handler other than its metadata
1381
+ * (see {@link CommandMetadataOptions}).
1382
+ */
1383
+ export interface CommandOptionsBase<State = unknown, Event = unknown, E2 = never, R2 = never, E4 = never, R4 = never> {
931
1384
  /**
932
1385
  * Identifier for this command delivery. Required when the handler was created
933
1386
  * with `idempotency`; a replayed id fails with `DuplicateCommand` — also when
@@ -964,7 +1417,7 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
964
1417
  readonly expectedVersion?: number | undefined;
965
1418
  /**
966
1419
  * Caller-owned transact items committed atomically with the produced events
967
- * (see {@link AppendOptions.additionalItems}).
1420
+ * (see {@link AppendOptionsBase.additionalItems}).
968
1421
  *
969
1422
  * Either a static array, or a function of the {@link Decision} — an inline
970
1423
  * projection — returning the array or an `Effect` of it. The function runs
@@ -974,31 +1427,80 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
974
1427
  * `AdditionalItemConditionFailed` refer to the array it returned.
975
1428
  */
976
1429
  readonly additionalItems?: AdditionalItemsInput<State, Event, E2, R2>;
1430
+ /**
1431
+ * Per-event timestamps derived from the {@link Decision}: the function
1432
+ * returns one entry per event, in order, directly or as an `Effect` (see
1433
+ * {@link EventTimestampsInput}), and entry `i` is written as event `i`'s
1434
+ * `timestamp` — `undefined` keeps the append's `Clock` time (see
1435
+ * {@link AppendOptionsBase.eventTimestamps}). Independent of `metadata` /
1436
+ * `eventMetadata`. The idempotency sentinel, its TTL and an inline snapshot
1437
+ * keep the append's `Clock` time; timestamps carry no ordering check.
1438
+ *
1439
+ * - Runs after `eventMetadata` and before `additionalItems`, each to
1440
+ * completion before the next, all receiving the same `Decision`; not
1441
+ * called when `decide` returns no events; re-run on every attempt — each
1442
+ * `VersionConflict` retry and the re-decision a stale unverified snapshot
1443
+ * triggers — as `eventMetadata` is.
1444
+ * - A failure of the returned `Effect` surfaces as is, is not retried, and
1445
+ * `additionalItems` is not run.
1446
+ * - A result — plain or produced by the `Effect` — of the wrong length, or
1447
+ * with an entry that is neither a valid `DateTime` (finite epoch) nor
1448
+ * `undefined`, fails with `ValidationError`
1449
+ * (`operation: "EventStore.commandHandler.eventTimestamps"`), never
1450
+ * retried. A value that is not a function is refused before anything is
1451
+ * read.
1452
+ *
1453
+ * ```ts
1454
+ * // plain: the times are in the events
1455
+ * eventTimestamps: ({ events }) => events.map((event) => event.occurredAt)
1456
+ *
1457
+ * // Effect: the times come from a service
1458
+ * eventTimestamps: ({ events }) =>
1459
+ * Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId))
1460
+ * ```
1461
+ */
1462
+ readonly eventTimestamps?: EventTimestampsInput<State, Event, E4, R4>;
977
1463
  }
1464
+ /**
1465
+ * Per-call options accepted by a handler produced by {@link commandHandler}:
1466
+ * {@link CommandOptionsBase} plus the exclusive `metadata` / `eventMetadata`
1467
+ * pair ({@link CommandMetadataOptions}).
1468
+ *
1469
+ * `State` / `Event` type the {@link Decision} a function-form `additionalItems`,
1470
+ * `eventMetadata` or `eventTimestamps` receives; `E2` / `R2` are the error and
1471
+ * requirements of the `Effect` `additionalItems` may return, `E3` / `R3` those
1472
+ * of the `Effect` `eventMetadata` may return, `E4` / `R4` those of the
1473
+ * `Effect` `eventTimestamps` may return — separate, so effects with different
1474
+ * error types all join the handler's error channel. The handler infers all of
1475
+ * them, so they only need naming when this type is written out by hand.
1476
+ */
1477
+ 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>;
978
1478
  /** Per-call options when the handler was created with `idempotency` — `commandId` is required. */
979
- export interface IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never> extends CommandOptions<TMetadata, State, Event, E2, R2> {
1479
+ 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> & {
980
1480
  readonly commandId: string;
981
- }
1481
+ };
982
1482
  /**
983
1483
  * Options arity: configuring `idempotency` makes the handler's options parameter
984
1484
  * required (and `commandId` within it non-optional), so a missing `commandId`
985
1485
  * is a compile error rather than a silent downgrade to at-least-once.
986
1486
  */
987
- type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2> = TConfig extends {
1487
+ type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2, E3, R3, E4, R4> = TConfig extends {
988
1488
  readonly idempotency: object;
989
- } ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2> | undefined];
1489
+ } ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4> | undefined];
990
1490
  type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
991
1491
  /**
992
1492
  * A handler over an {@link EventStream}. Generic per call over the error (`E2`)
993
1493
  * and requirements (`R2`) of a function-form `additionalItems` that returns an
994
- * `Effect`; both are `never` otherwise.
1494
+ * `Effect`, those (`E3` / `R3`) of an `eventMetadata` that does, and those
1495
+ * (`E4` / `R4`) of an `eventTimestamps` that does; all are `never` otherwise.
995
1496
  */
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>;
1497
+ 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>;
997
1498
  /**
998
1499
  * A handler over a {@link BoundEventStream}: the stream's services are already
999
- * provided, so a function-form `additionalItems`' `R2` is its only requirement.
1500
+ * provided, so the `R2` / `R3` / `R4` of a function-form `additionalItems` /
1501
+ * `eventMetadata` / `eventTimestamps` are its only requirements.
1000
1502
  */
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>;
1503
+ 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>;
1002
1504
  /**
1003
1505
  * The handler the data-last {@link commandHandler} returns for stream `S`: a
1004
1506
  * {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
@@ -1042,6 +1544,24 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1042
1544
  * yield* handle({ matchId: "m-1" }, command, {
1043
1545
  * additionalItems: ({ state }) => [Scoreboard.put({ matchId: "m-1", runs: state.runs })],
1044
1546
  * })
1547
+ *
1548
+ * // Per-event metadata derived from the decision: entry i is written onto event i
1549
+ * // (`crypto` is the `Crypto.Crypto` service; `randomUUIDv7` works the same way)
1550
+ * yield* handle({ matchId: "m-1" }, command, {
1551
+ * eventMetadata: ({ events }) =>
1552
+ * Effect.forEach(events, () => Effect.map(crypto.randomULID, (eventId) => ({ eventId }))),
1553
+ * })
1554
+ *
1555
+ * // Per-event timestamps derived from the decision (undefined → the Clock time)
1556
+ * yield* handle({ matchId: "m-1" }, command, {
1557
+ * eventTimestamps: ({ events }) => events.map((event) => event.occurredAt),
1558
+ * })
1559
+ *
1560
+ * // ...or from a service, as an Effect (its errors / requirements join the handler's)
1561
+ * yield* handle({ matchId: "m-1" }, command, {
1562
+ * eventTimestamps: ({ events }) =>
1563
+ * Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId)),
1564
+ * })
1045
1565
  * ```
1046
1566
  *
1047
1567
  * Each invocation runs
@@ -1057,7 +1577,7 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1057
1577
  * item alone (one `GetItem`); an answer that is not a successful append is
1058
1578
  * confirmed against the head first, and a snapshot that proves stale falls
1059
1579
  * back to the verified load — in which case `decide` may run twice.
1060
- * - **expectedVersion.** See {@link CommandOptions.expectedVersion}.
1580
+ * - **expectedVersion.** See {@link CommandOptionsBase.expectedVersion}.
1061
1581
  * - **Fold before append.** The new events are folded into state before the
1062
1582
  * append, so the state returned, snapshotted and handed to a function-form
1063
1583
  * `additionalItems` (see {@link Decision}) is always the `evolve` fold.
@@ -1067,8 +1587,8 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1067
1587
  * fresh snapshot is written (best-effort) after a successful append once the
1068
1588
  * cadence threshold is crossed. See {@link SnapshotMode}.
1069
1589
  * - **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
1590
+ * too large for one transaction is rejected whole by DynamoDB and fails with
1591
+ * `AppendTooLarge` — nothing is written. A large command is split by the application into stepped
1072
1592
  * commands — each an ordinary call, chained by `expectedVersion` — so a
1073
1593
  * failure partway leaves the stream at a real intermediate state.
1074
1594
  *
@@ -1087,9 +1607,13 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1087
1607
  * `EventStreamTypeId` brand of the second argument.
1088
1608
  */
1089
1609
  export declare const commandHandler: {
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>;
1610
+ <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>;
1611
+ <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1612
+ readonly [K in TIndexName]: StreamIndexProjection;
1613
+ }, 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>;
1614
+ <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1615
+ readonly [K in TIndexName]: StreamIndexProjection;
1616
+ }, 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>;
1093
1617
  };
1094
1618
  /**
1095
1619
  * Reconstruct state from events by folding through a decider's `evolve` function.
@@ -1109,5 +1633,4 @@ export declare const foldFrom: {
1109
1633
  <A>(startState: unknown, events: ReadonlyArray<StreamEvent<A>>): <S, C, E>(decider: Decider<S, C, A, E>) => S;
1110
1634
  <S, C, A, E>(decider: Decider<S, C, A, E>, startState: S, events: ReadonlyArray<StreamEvent<A>>): S;
1111
1635
  };
1112
- export {};
1113
1636
  //# sourceMappingURL=EventStore.d.ts.map