effect-dynamodb 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/Aggregate.d.ts +5 -2
  2. package/dist/Aggregate.d.ts.map +1 -1
  3. package/dist/Aggregate.js +136 -53
  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.map +1 -1
  10. package/dist/Collection.js +2 -4
  11. package/dist/Collection.js.map +1 -1
  12. package/dist/DynamoClient.d.ts.map +1 -1
  13. package/dist/DynamoClient.js +42 -14
  14. package/dist/DynamoClient.js.map +1 -1
  15. package/dist/Entity.d.ts +19 -13
  16. package/dist/Entity.d.ts.map +1 -1
  17. package/dist/Entity.js +287 -226
  18. package/dist/Entity.js.map +1 -1
  19. package/dist/EventStore.d.ts +416 -73
  20. package/dist/EventStore.d.ts.map +1 -1
  21. package/dist/EventStore.js +525 -181
  22. package/dist/EventStore.js.map +1 -1
  23. package/dist/Query.d.ts +47 -14
  24. package/dist/Query.d.ts.map +1 -1
  25. package/dist/Query.js +190 -68
  26. package/dist/Query.js.map +1 -1
  27. package/dist/Table.d.ts +56 -6
  28. package/dist/Table.d.ts.map +1 -1
  29. package/dist/Table.js +134 -54
  30. package/dist/Table.js.map +1 -1
  31. package/dist/Transaction.d.ts +8 -0
  32. package/dist/Transaction.d.ts.map +1 -1
  33. package/dist/Transaction.js +67 -22
  34. package/dist/Transaction.js.map +1 -1
  35. package/dist/VectorSearchEmulation.d.ts +3 -2
  36. package/dist/VectorSearchEmulation.d.ts.map +1 -1
  37. package/dist/VectorSearchEmulation.js +14 -2
  38. package/dist/VectorSearchEmulation.js.map +1 -1
  39. package/dist/internal/BoundQuery.d.ts +3 -2
  40. package/dist/internal/BoundQuery.d.ts.map +1 -1
  41. package/dist/internal/BoundQuery.js.map +1 -1
  42. package/dist/internal/BoundVectorQuery.d.ts +5 -1
  43. package/dist/internal/BoundVectorQuery.d.ts.map +1 -1
  44. package/dist/internal/BoundVectorQuery.js +17 -5
  45. package/dist/internal/BoundVectorQuery.js.map +1 -1
  46. package/dist/internal/EntityCombinators.d.ts +8 -5
  47. package/dist/internal/EntityCombinators.d.ts.map +1 -1
  48. package/dist/internal/EntityCombinators.js +8 -5
  49. package/dist/internal/EntityCombinators.js.map +1 -1
  50. package/dist/internal/EntityOps.d.ts +1 -1
  51. package/dist/internal/Expr.d.ts +4 -0
  52. package/dist/internal/Expr.d.ts.map +1 -1
  53. package/dist/internal/Expr.js +6 -4
  54. package/dist/internal/Expr.js.map +1 -1
  55. package/dist/internal/ExpressionLimits.d.ts +45 -0
  56. package/dist/internal/ExpressionLimits.d.ts.map +1 -0
  57. package/dist/internal/ExpressionLimits.js +89 -0
  58. package/dist/internal/ExpressionLimits.js.map +1 -0
  59. package/dist/internal/Hydrate.d.ts +62 -0
  60. package/dist/internal/Hydrate.d.ts.map +1 -0
  61. package/dist/internal/Hydrate.js +191 -0
  62. package/dist/internal/Hydrate.js.map +1 -0
  63. package/dist/internal/ItemSize.d.ts +60 -24
  64. package/dist/internal/ItemSize.d.ts.map +1 -1
  65. package/dist/internal/ItemSize.js +122 -39
  66. package/dist/internal/ItemSize.js.map +1 -1
  67. package/dist/internal/TransactWriteOps.d.ts +22 -46
  68. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  69. package/dist/internal/TransactWriteOps.js +26 -95
  70. package/dist/internal/TransactWriteOps.js.map +1 -1
  71. package/dist/internal/WriteDiagnosis.d.ts +353 -0
  72. package/dist/internal/WriteDiagnosis.d.ts.map +1 -0
  73. package/dist/internal/WriteDiagnosis.js +1028 -0
  74. package/dist/internal/WriteDiagnosis.js.map +1 -0
  75. package/dist/internal/sparse_check.js +43 -79
  76. package/dist/internal/sparse_check.js.map +1 -1
  77. package/package.json +2 -2
@@ -13,8 +13,9 @@
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
19
20
  *
20
21
  * Built on the existing library primitives (DynamoSchema, KeyComposer, Query,
@@ -23,7 +24,7 @@
23
24
  import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
24
25
  import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
25
26
  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";
27
+ import { DateTime, type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
27
28
  import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
28
29
  import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
29
30
  import * as Query from "./Query.js";
@@ -58,6 +59,11 @@ export interface StreamEvent<A, M = Record<string, unknown> | undefined> {
58
59
  readonly eventType: string;
59
60
  readonly data: A;
60
61
  readonly metadata: M;
62
+ /**
63
+ * ISO 8601 (UTC): when the event was appended, by the append's `Clock` — or
64
+ * the `eventTimestamps` entry it was appended with
65
+ * ({@link AppendOptionsBase.eventTimestamps}).
66
+ */
61
67
  readonly timestamp: string;
62
68
  }
63
69
  /**
@@ -130,7 +136,7 @@ export interface SnapshotConfig<TSchema extends Schema.Top = Schema.Top> {
130
136
  * succeeds, at the `every` cadence. A failed snapshot write never fails the
131
137
  * command; the snapshot simply lags until the next threshold crossing.
132
138
  * - `"inline"` — the snapshot `Put` rides in the append's own transaction (see
133
- * {@link AppendOptions.snapshot}), on every append, or at the `every` cadence
139
+ * {@link AppendOptionsBase.snapshot}), on every append, or at the `every` cadence
134
140
  * when one is set. The snapshot is then current after every command that
135
141
  * writes one, so {@link EventStream.readLatest} loads state in one request.
136
142
  * The snapshot counts towards the transaction's item and size limits.
@@ -153,7 +159,7 @@ export interface SnapshotSettings {
153
159
  * redelivery whose `decide` returns no events against the loaded state never
154
160
  * appends, so it succeeds as a no-op instead of failing with
155
161
  * `DuplicateCommand`. Whether a no-op redelivery needs to be told apart is the
156
- * application's call (see {@link CommandOptions.commandId}).
162
+ * application's call (see {@link CommandOptionsBase.commandId}).
157
163
  *
158
164
  * The sentinel is co-located in the stream's own partition, so `commandId`
159
165
  * uniqueness is scoped to the stream (which is what "have I already applied this
@@ -178,14 +184,46 @@ export interface AppendIdempotency {
178
184
  readonly ttl?: Duration.Duration | string;
179
185
  }
180
186
  /**
181
- * Options accepted by {@link EventStream.append}.
187
+ * The metadata of an {@link EventStream.append}: one value shared by every
188
+ * event (`metadata`), or one entry per event (`eventMetadata`) — never both.
189
+ *
190
+ * Passing both is a compile error, and a `ValidationError` at runtime for an
191
+ * untyped caller. `undefined` counts as absent for either, so
192
+ * `{ metadata: undefined, eventMetadata: [...] }` is not "both".
193
+ */
194
+ export type AppendMetadataOptions<TMetadata> = {
195
+ /**
196
+ * Metadata written onto **every** event of the append — a copy per item,
197
+ * encoded once through the stream's metadata schema when one is
198
+ * configured. For metadata that belongs to each event (an event id, the
199
+ * original occurrence time of an imported event) use `eventMetadata`:
200
+ * packing a list into `metadata` stores every entry on every item, so
201
+ * stored metadata grows with the square of the append size.
202
+ */
203
+ readonly metadata?: TMetadata;
204
+ readonly eventMetadata?: undefined;
205
+ } | {
206
+ readonly metadata?: undefined;
207
+ /**
208
+ * Per-event metadata: entry `i` is written onto event `i` (and read back
209
+ * as that event's `StreamEvent.metadata`). Each defined entry is encoded
210
+ * through the stream's metadata schema, as `metadata` is; an `undefined`
211
+ * entry writes its event without a `metadata` attribute.
212
+ *
213
+ * Must have exactly one entry per event — a length mismatch fails with
214
+ * `ValidationError` before anything is read or written, also for an
215
+ * append of no events (where only `[]` is accepted).
216
+ */
217
+ readonly eventMetadata?: ReadonlyArray<TMetadata | undefined>;
218
+ };
219
+ /**
220
+ * The options of {@link EventStream.append} other than its metadata (see
221
+ * {@link AppendMetadataOptions}).
182
222
  *
183
223
  * `TState` is the stream's snapshot state type (`never` for a stream without a
184
224
  * `snapshot` config, so `snapshot` cannot be supplied there).
185
225
  */
186
- export interface AppendOptions<TMetadata, TState = never> {
187
- /** Per-append metadata, validated against the stream's metadata schema when configured. */
188
- readonly metadata?: TMetadata;
226
+ export interface AppendOptionsBase<TState = never> {
189
227
  /**
190
228
  * Caller-owned transact items committed atomically with the events — the same
191
229
  * op union `Transaction.transactWrite` accepts (`EntityPut`, `EntityDelete`,
@@ -224,7 +262,34 @@ export interface AppendOptions<TMetadata, TState = never> {
224
262
  * is `never`) it dies with `[EDD-9026]`, as `writeSnapshot` does.
225
263
  */
226
264
  readonly snapshot?: TState;
265
+ /**
266
+ * Per-event timestamps: entry `i` is written as event `i`'s `timestamp` (and
267
+ * read back as that event's `StreamEvent.timestamp`), for history imported
268
+ * with its original occurrence times. An `undefined` entry keeps the
269
+ * append's `Clock` time. Independent of `metadata` / `eventMetadata`.
270
+ *
271
+ * Written with `DateTime.formatIso` — the format the append's own time is
272
+ * written in — so a zoned `DateTime` is stored as its UTC instant.
273
+ *
274
+ * - Must have exactly one entry per event, each a valid `DateTime` (finite
275
+ * epoch) or `undefined` — anything else fails with `ValidationError` before anything is read or
276
+ * written, also for an append of no events (where only `[]` is accepted).
277
+ * - Only the event items change: the idempotency sentinel, its TTL and the
278
+ * inline snapshot record when the append was written.
279
+ * - No ordering is enforced. `version` orders a stream; timestamps may
280
+ * repeat, run backwards or lie in the future, as imported history can.
281
+ */
282
+ readonly eventTimestamps?: ReadonlyArray<DateTime.DateTime | undefined>;
227
283
  }
284
+ /**
285
+ * Options accepted by {@link EventStream.append}: {@link AppendOptionsBase}
286
+ * plus the exclusive `metadata` / `eventMetadata` pair
287
+ * ({@link AppendMetadataOptions}).
288
+ *
289
+ * A type alias rather than an interface, since it is an intersection with a
290
+ * union: an interface cannot extend it.
291
+ */
292
+ export type AppendOptions<TMetadata, TState = never> = AppendOptionsBase<TState> & AppendMetadataOptions<TMetadata>;
228
293
  /** Error channel of {@link EventStream.append}. */
229
294
  export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
230
295
  /**
@@ -298,8 +363,10 @@ export interface LatestState<TState, TEvent, M = Record<string, unknown> | undef
298
363
  * - `undefined` leaves the event out of the index (a sparse index).
299
364
  * - An empty string (or any other value) fails the append with
300
365
  * `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.
366
+ * values — as does a `key` that throws. A key over DynamoDB's 1024-byte
367
+ * sort-key limit is sent and rejected by DynamoDB; the append then fails
368
+ * with `ValidationError` naming the index and the event (#144), and nothing
369
+ * is written.
303
370
  *
304
371
  * The key is derived only when an event is appended. Events are never
305
372
  * rewritten, so changing `key` later leaves events already stored under their
@@ -322,9 +389,9 @@ export type StreamIndexKey<TEvent> = (event: TEvent, version: number) => string
322
389
  * An LSI must be created with the table. A table with any LSI caps the item
323
390
  * collection of **every** partition key value in it at 10 GB — not only the
324
391
  * 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}.
392
+ * entity partition sharing the table; a write that would exceed it fails. What
393
+ * the index copies from each event is its `projection` — see
394
+ * {@link StreamIndexProjection}.
328
395
  */
329
396
  export interface LocalStreamIndexConfig<TEvent> {
330
397
  readonly type?: "lsi" | undefined;
@@ -333,13 +400,19 @@ export interface LocalStreamIndexConfig<TEvent> {
333
400
  /** An LSI uses the table partition key; supplying `pk` is refused (`[EDD-9063]`). */
334
401
  readonly pk?: undefined;
335
402
  readonly key: StreamIndexKey<TEvent>;
403
+ /**
404
+ * What the index copies from each event — see {@link StreamIndexProjection}.
405
+ * Default `"all"`. Must match the deployed index.
406
+ */
407
+ readonly projection?: StreamIndexProjection | undefined;
336
408
  }
337
409
  /**
338
410
  * A stream index on a **global** secondary index. `append` writes the stream's
339
411
  * partition key value into the `pk` attribute of each indexed event, so the
340
412
  * 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`.
413
+ * an LSI, without the LSI's 10 GB item-collection cap. DynamoDB rejects a
414
+ * strongly consistent read of it, diagnosed as a `ValidationError` naming the
415
+ * index.
343
416
  *
344
417
  * Unlike an LSI, the GSI itself can be added to an existing table
345
418
  * (`UpdateTable`), but that does not index the stream's existing events: the
@@ -350,6 +423,7 @@ export interface LocalStreamIndexConfig<TEvent> {
350
423
  * - `pk` — the attribute carrying the stream's partition key (the GSI's HASH key).
351
424
  * - `sk` — the attribute carrying the derived key (the GSI's RANGE key).
352
425
  * - `key` — see {@link StreamIndexKey}.
426
+ * - `projection` — see {@link StreamIndexProjection}.
353
427
  */
354
428
  export interface GlobalStreamIndexConfig<TEvent> {
355
429
  readonly type: "gsi";
@@ -357,6 +431,11 @@ export interface GlobalStreamIndexConfig<TEvent> {
357
431
  readonly pk: string;
358
432
  readonly sk: string;
359
433
  readonly key: StreamIndexKey<TEvent>;
434
+ /**
435
+ * What the index copies from each event — see {@link StreamIndexProjection}.
436
+ * Default `"all"`. Must match the deployed index.
437
+ */
438
+ readonly projection?: StreamIndexProjection | undefined;
360
439
  }
361
440
  /**
362
441
  * One entry of `makeStream({ indexes })`: a sub-stream of the stream's events,
@@ -364,30 +443,95 @@ export interface GlobalStreamIndexConfig<TEvent> {
364
443
  * and {@link GlobalStreamIndexConfig}.
365
444
  */
366
445
  export type StreamIndexConfig<TEvent> = LocalStreamIndexConfig<TEvent> | GlobalStreamIndexConfig<TEvent>;
446
+ /**
447
+ * What a stream index copies from each event (`projection` on an entry of
448
+ * `makeStream({ indexes })`). {@link indexDefinitions} emits the matching
449
+ * `Projection`, and the reads choose their path from it.
450
+ *
451
+ * | Value | Emitted `Projection` | Read |
452
+ * |---|---|---|
453
+ * | `"all"` (default) | `ALL` | one query |
454
+ * | `"withoutMetadata"` | `INCLUDE` of every event attribute except `metadata` | one query; `metadata` is `undefined` (and typed so) |
455
+ * | `"keys"` | `INCLUDE ["__edd_e__"]` — **not** `KEYS_ONLY` | LSI: one query with `Select: ALL_ATTRIBUTES` (DynamoDB fetches each event from the table). GSI: the query, then `BatchGetItem` for the events of each page, in index order |
456
+ *
457
+ * `__edd_e__` is always projected because every read filters on it, and
458
+ * DynamoDB refuses a filter on an attribute an index does not project.
459
+ *
460
+ * `metadata` is often an event's largest attribute; `"withoutMetadata"` keeps
461
+ * it out of the index, and `"keys"` keeps the index entries minimal at the cost
462
+ * of a table read per event read (rounded up to 4 KB each).
463
+ *
464
+ * **The declared projection must match the deployed index.** The library does
465
+ * not migrate: an LSI's projection is fixed when the table is created, and a
466
+ * GSI's cannot be updated (it must be deleted and created again). Tables
467
+ * deployed with CDK or Terraform copy `Projection` from
468
+ * {@link indexDefinitions}' output. An index deployed narrower than declared
469
+ * fails reads: DynamoDB refuses the `__edd_e__` filter ("does not project one
470
+ * or more filter attributes"), or rows lack event attributes, which the read
471
+ * reports as a `ValidationError` (`operation: "EventStore.index.decode"`)
472
+ * naming the index.
473
+ */
474
+ export type StreamIndexProjection = "all" | "withoutMetadata" | "keys";
367
475
  /**
368
476
  * The runtime settings of one stream index, as exposed on
369
477
  * {@link EventStream.indexes}. `pk` is the index's partition key attribute:
370
478
  * the table's `pk` for an LSI, the configured attribute for a GSI.
479
+ * `projection` is resolved: `"all"` when the index declares none.
371
480
  */
372
481
  export interface StreamIndexSettings {
373
482
  readonly type: "lsi" | "gsi";
374
483
  readonly index: string;
375
484
  readonly pk: string;
376
485
  readonly sk: string;
486
+ readonly projection: StreamIndexProjection;
377
487
  }
488
+ /**
489
+ * The `metadata` type of an event read through a stream index of projection
490
+ * `P`: `undefined` for a `"withoutMetadata"` index, which does not hold it,
491
+ * the stream's {@link StreamMetadata} otherwise.
492
+ */
493
+ export type IndexEventMetadata<TMetadata, P extends StreamIndexProjection> = [P] extends [
494
+ "withoutMetadata"
495
+ ] ? undefined : StreamMetadata<TMetadata>;
496
+ /** @internal The `projection` an index config declares (`"all"` when none). */
497
+ type ProjectionOf<C> = C extends {
498
+ readonly projection: infer P extends StreamIndexProjection;
499
+ } ? P : "all";
500
+ /**
501
+ * The projection of each index of a `makeStream({ indexes })` config, by name.
502
+ * `"all"` for an index that declares none.
503
+ */
504
+ export type IndexProjectionsOf<TIndexes> = {
505
+ readonly [K in keyof TIndexes & string]: ProjectionOf<TIndexes[K]>;
506
+ };
507
+ /** @internal The members a stream index entry may declare. */
508
+ type StreamIndexConfigKey = keyof LocalStreamIndexConfig<unknown> | keyof GlobalStreamIndexConfig<unknown>;
509
+ /**
510
+ * The `indexes` parameter of {@link makeStream}: each entry is a
511
+ * {@link StreamIndexConfig}, and a member it does not declare (a misspelled
512
+ * `projection`, say) is typed `never`, so it is a compile error. Inferring
513
+ * `TIndexes` from the entries turns off TypeScript's own excess-property check,
514
+ * which would otherwise catch it.
515
+ */
516
+ export type StreamIndexesConfig<TIndexes, TEvent> = {
517
+ readonly [K in keyof TIndexes]: StreamIndexConfig<TEvent> & {
518
+ readonly [P in Exclude<keyof TIndexes[K], StreamIndexConfigKey>]: never;
519
+ };
520
+ };
378
521
  /**
379
522
  * Options accepted by {@link EventStream.readIndex}.
380
523
  *
381
524
  * - `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.
525
+ * one of them, string bounds). Omitted, the whole sub-stream is read.
526
+ * DynamoDB rejects an empty bound, and a `between` whose lower bound sorts
527
+ * (by UTF-8 bytes) after its upper bound; the read then fails with a
528
+ * `ValidationError` naming the index and the bound, DynamoDB's error as its
529
+ * cause.
386
530
  * - `reverse` — descending key order.
387
531
  * - `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.
532
+ * - `consistentRead` — strongly consistent read. DynamoDB rejects it on a
533
+ * GSI-backed index, which it reads only eventually consistently: the read
534
+ * fails with a `ValidationError` naming the index.
391
535
  */
392
536
  export type ReadIndexOptions = {
393
537
  readonly reverse?: boolean | undefined;
@@ -413,9 +557,15 @@ export type EventStreamTypeId = typeof EventStreamTypeId;
413
557
  *
414
558
  * `TIndexName` is the union of the stream's index names (`makeStream({ indexes })`),
415
559
  * `never` for a stream without indexes, so {@link EventStream.readIndex} and
416
- * `query.index` only accept a declared name.
560
+ * `query.index` only accept a declared name. `TIndexProjections` is each
561
+ * index's {@link StreamIndexProjection} by name, so an event read through a
562
+ * `"withoutMetadata"` index has `metadata: undefined`.
417
563
  */
418
- export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
564
+ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
565
+ readonly [K in TIndexName]: StreamIndexProjection;
566
+ } = {
567
+ readonly [K in TIndexName]: "all";
568
+ }> extends Pipeable.Pipeable {
419
569
  readonly [EventStreamTypeId]: EventStreamTypeId;
420
570
  readonly streamName: string;
421
571
  readonly eventSchema: Schema.Top;
@@ -515,22 +665,35 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
515
665
  * Read the stream's events in the order of one of its indexes (#140) — the
516
666
  * events whose index `key` returned a string, ordered by that key. A
517
667
  * 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}.
668
+ * the key range, `reverse`, `limit` and `consistentRead` (which DynamoDB
669
+ * rejects on a GSI; the error names the index) — see {@link ReadIndexOptions}.
670
+ *
671
+ * How the events are read follows the index's {@link StreamIndexProjection}:
672
+ * one query for `"all"` and `"withoutMetadata"` (whose events have
673
+ * `metadata: undefined`, as the type says); for `"keys"`, one query with
674
+ * `Select: ALL_ATTRIBUTES` on an LSI, or the query followed by `BatchGetItem`
675
+ * (unprocessed keys retried, index order kept) on a GSI.
520
676
  */
521
- readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
677
+ readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
522
678
  readonly query: {
523
679
  events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
524
680
  /**
525
681
  * A query of one of the stream's indexes (#140), scoped to `streamId`:
526
682
  * decoded events in index-key order. It composes with the `Query`
527
683
  * 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
684
+ * `Query.reverse`, `Query.limit`, `Query.consistentRead` (DynamoDB rejects
685
+ * it on a GSI-backed index: a `ValidationError` naming the index) — and terminals
530
686
  * (`Query.collect`, `Query.execute`, `Query.paginate`). The
531
687
  * `__edd_e__ = <stream>.event` filter still applies.
688
+ *
689
+ * It reads as {@link EventStream.readIndex} does for the index's
690
+ * projection: a `"keys"` GSI fetches each page's events with
691
+ * `BatchGetItem` (`Query.select` moves onto that fetch); `Query.count`
692
+ * counts index entries and fetches nothing. On a `"keys"` or
693
+ * `"withoutMetadata"` index, a `Query.filter` on an attribute the index
694
+ * does not project is refused by DynamoDB — use `Query.filterBy`.
532
695
  */
533
- index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
696
+ index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
534
697
  };
535
698
  }
536
699
  /**
@@ -592,7 +755,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
592
755
  * event's derived key (and, for a GSI, the stream's partition key) on the
593
756
  * event item; snapshots and idempotency sentinels never carry them, so they
594
757
  * are never in an index. Read with {@link EventStream.readIndex} or
595
- * `query.index`, and create the indexes with {@link indexDefinitions}.
758
+ * `query.index`, and create the indexes with {@link indexDefinitions}. Each
759
+ * index's `projection` (default `"all"`) sets what it copies from each event
760
+ * and how it is read — see {@link StreamIndexProjection}.
596
761
  *
597
762
  * Index attributes are written only at append time and events are never
598
763
  * rewritten. Declaring an index on a stream that already holds events, or
@@ -629,7 +794,9 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
629
794
  * `"inline"`.
630
795
  * @throws `[EDD-9063]` for a malformed index: a `type` other than `"lsi"` /
631
796
  * `"gsi"`, a `gsi` without `pk`, an `lsi` with `pk`, an empty `index` / `sk`
632
- * / `pk`, or a `key` that is not a function.
797
+ * / `pk`, a `key` that is not a function, a `projection` other than
798
+ * `"all"` / `"withoutMetadata"` / `"keys"`, or a member an index entry does
799
+ * not declare (such as a misspelled `projection`, also a type error).
633
800
  * @throws `[EDD-9064]` when an index attribute is one the stream writes itself
634
801
  * (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
635
802
  * `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
@@ -641,7 +808,7 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
641
808
  */
642
809
  export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top>, TTable extends Table, const TStreamName extends string, const TStreamId extends {
643
810
  readonly composite: ReadonlyArray<string>;
644
- }, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexName extends string = never>(config: {
811
+ }, TMetadata extends Schema.Top | undefined = undefined, TSnapshot extends SnapshotConfig | undefined = undefined, const TIndexes extends { readonly [K in keyof TIndexes]: StreamIndexConfig<Schema.Schema.Type<TEvents[number]>>; } = {}>(config: {
645
812
  readonly table: TTable;
646
813
  readonly streamName: TStreamName;
647
814
  readonly events: TEvents;
@@ -649,8 +816,8 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
649
816
  readonly metadata?: TMetadata;
650
817
  readonly snapshot?: TSnapshot;
651
818
  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>;
819
+ readonly indexes?: TIndexes & StreamIndexesConfig<TIndexes, Schema.Schema.Type<TEvents[number]>>;
820
+ }) => EventStream<Schema.Schema.Type<TEvents[number]>, TStreamId["composite"], TMetadata extends Schema.Top ? Schema.Schema.Type<TMetadata> : undefined, TSnapshot extends SnapshotConfig<infer TStateSchema> ? Schema.Schema.Type<TStateSchema> : never, keyof TIndexes & string, IndexProjectionsOf<TIndexes>>;
654
821
  /**
655
822
  * An EventStream whose operations have `DynamoClient` and `TableConfig` already
656
823
  * resolved, so all methods return `Effect<A, E, never>`.
@@ -671,7 +838,11 @@ export declare const makeStream: <const TEvents extends ReadonlyArray<Schema.Top
671
838
  * }) {}
672
839
  * ```
673
840
  */
674
- export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never> extends Pipeable.Pipeable {
841
+ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState = never, TIndexName extends string = never, TIndexProjections extends {
842
+ readonly [K in TIndexName]: StreamIndexProjection;
843
+ } = {
844
+ readonly [K in TIndexName]: "all";
845
+ }> extends Pipeable.Pipeable {
675
846
  readonly [EventStreamTypeId]: EventStreamTypeId;
676
847
  readonly streamName: string;
677
848
  readonly eventSchema: Schema.Top;
@@ -696,7 +867,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
696
867
  /** See {@link EventStream.readLatest}. */
697
868
  readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, never>;
698
869
  /** See {@link EventStream.readIndex}. */
699
- readIndex(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
870
+ readIndex<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>, options?: ReadIndexOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>, DynamoClientError | ValidationError, never>;
700
871
  /**
701
872
  * The stream's query builders — unbound: run them with {@link provide}, or
702
873
  * where `DynamoClient` and `TableConfig` are provided.
@@ -704,7 +875,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
704
875
  readonly query: {
705
876
  events(streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
706
877
  /** See `EventStream.query.index`. */
707
- index(name: TIndexName, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, StreamMetadata<TMetadata>>>;
878
+ index<N extends TIndexName>(name: N, streamId: StreamIdInput<TStreamIdFields>): Query.Query<StreamEvent<TEvent, IndexEventMetadata<TMetadata, TIndexProjections[N]>>>;
708
879
  };
709
880
  /** Escape hatch: provide DynamoClient | TableConfig to an arbitrary effect. */
710
881
  readonly provide: <A, E>(effect: Effect.Effect<A, E, DynamoClient | TableConfig>) => Effect.Effect<A, E, never>;
@@ -723,7 +894,7 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
723
894
  * yield* stream.append({ matchId: "m-1" }, [event], 0) // R = never
724
895
  * ```
725
896
  */
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>;
897
+ export declare const bind: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState, TIndexName extends string = never, TIndexProjections extends { readonly [K in TIndexName]: StreamIndexProjection; } = { readonly [K in TIndexName]: "all"; }>(stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>) => Effect.Effect<BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, never, DynamoClient | TableConfig>;
727
898
  /**
728
899
  * `CreateTable` fragments for the stream indexes of one or more streams — see
729
900
  * {@link indexDefinitions}. The lists are absent when empty, because
@@ -738,11 +909,26 @@ export interface StreamIndexDefinitions {
738
909
  /**
739
910
  * Derive the `CreateTable` fragments for the stream indexes (#140) of the given
740
911
  * 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.
912
+ * `LocalSecondaryIndexes` and `GlobalSecondaryIndexes` — to merge into the
913
+ * caller's `CreateTable` input. Event tables are not derived by
914
+ * `Table.definition`, and the table's own `pk` / `sk` are not included: add
915
+ * them (and, on a provisioned table, each GSI's `ProvisionedThroughput`)
916
+ * yourself.
917
+ *
918
+ * Each index's `Projection` follows its {@link StreamIndexProjection}:
919
+ *
920
+ * | `projection` | `Projection` |
921
+ * |---|---|
922
+ * | `"all"` (default) | `{ ProjectionType: "ALL" }` |
923
+ * | `"withoutMetadata"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__", "data", "eventType", "streamId", "timestamp", "version"] }` |
924
+ * | `"keys"` | `{ ProjectionType: "INCLUDE", NonKeyAttributes: ["__edd_e__"] }` — not `KEYS_ONLY`: every read filters on `__edd_e__` |
925
+ *
926
+ * The deployed index must project what is declared — copy `Projection` from
927
+ * this output into CDK or Terraform definitions. Neither an LSI's nor a GSI's
928
+ * projection can be changed in place, and the library migrates nothing. A
929
+ * table projects at most 100 `NonKeyAttributes` across all its indexes;
930
+ * `"withoutMetadata"` spends 6 of them and `"keys"` 1 (DynamoDB refuses a
931
+ * table over the limit).
746
932
  *
747
933
  * An index shared by several streams with the same definition is emitted
748
934
  * once. Lists are sorted by index name, attributes by attribute name.
@@ -750,7 +936,6 @@ export interface StreamIndexDefinitions {
750
936
  * - An LSI can only be created with the table, and caps the item collection
751
937
  * of every partition key value in the table at 10 GB — entity partitions
752
938
  * and streams without indexes included.
753
- * - The projection must stay `ALL`: events are decoded from the index item.
754
939
  *
755
940
  * @example
756
941
  * ```typescript
@@ -773,12 +958,15 @@ export interface StreamIndexDefinitions {
773
958
  * ```
774
959
  *
775
960
  * @throws `[EDD-9067]` when two streams define the same physical index
776
- * differently (another type, or other key attributes).
961
+ * differently (another type, other key attributes, or another projection).
777
962
  */
778
963
  export declare const indexDefinitions: (...streams: ReadonlyArray<{
779
964
  readonly streamName: string;
780
965
  readonly indexes: {
781
- readonly [name: string]: StreamIndexSettings;
966
+ readonly [name: string]: Omit<StreamIndexSettings, "projection"> & {
967
+ /** Read as `"all"` when absent (a hand-built settings object). */
968
+ readonly projection?: StreamIndexProjection | undefined;
969
+ };
782
970
  };
783
971
  }>) => StreamIndexDefinitions;
784
972
  /**
@@ -803,7 +991,7 @@ export interface CommandHandlerOptions {
803
991
  * initial attempt). Omit for the default: no retry.
804
992
  *
805
993
  * `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
806
- * any call that supplies {@link CommandOptions.expectedVersion}: the caller asked
994
+ * any call that supplies {@link CommandOptionsBase.expectedVersion}: the caller asked
807
995
  * for a conditional write, so its `VersionConflict` is always surfaced.
808
996
  */
809
997
  readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
@@ -850,7 +1038,7 @@ export interface CommandHandlerOptions {
850
1038
  *
851
1039
  * - **No snapshot item** (a stream not yet snapshotted): the verified load
852
1040
  * runs at once, and `decide` runs once.
853
- * - **With {@link CommandOptions.expectedVersion}**: a snapshot whose
1041
+ * - **With {@link CommandOptionsBase.expectedVersion}**: a snapshot whose
854
1042
  * `asOfVersion` differs from it is re-read verified before the If-Match
855
1043
  * check, so only a genuine mismatch is reported (its `actualVersion` is
856
1044
  * the verified head). A snapshot at the expected version holds exactly the
@@ -890,7 +1078,9 @@ export interface CommandHandlerOptions {
890
1078
  }
891
1079
  /**
892
1080
  * The outcome of one `decide` call, handed to a function-form
893
- * {@link CommandOptions.additionalItems}.
1081
+ * {@link CommandOptionsBase.additionalItems}, to `eventMetadata`
1082
+ * ({@link CommandMetadataOptions}) and to
1083
+ * {@link CommandOptionsBase.eventTimestamps}.
894
1084
  *
895
1085
  * - `events` — what `decide` returned (never empty: the function is not called
896
1086
  * for a no-op decision).
@@ -911,7 +1101,7 @@ export interface Decision<State, Event> {
911
1101
  readonly version: number;
912
1102
  }
913
1103
  /**
914
- * The forms {@link CommandOptions.additionalItems} accepts: a static array of
1104
+ * The forms {@link CommandOptionsBase.additionalItems} accepts: a static array of
915
1105
  * transact ops, or a function of the {@link Decision} returning the array
916
1106
  * directly (pure projection) or as an `Effect` (a projection that needs a read).
917
1107
  * The effect's `E2` joins the handler's error channel and its `R2` the handler's
@@ -919,15 +1109,97 @@ export interface Decision<State, Event> {
919
1109
  */
920
1110
  export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
921
1111
  /**
922
- * Per-call options accepted by a handler produced by {@link commandHandler}.
1112
+ * The form `eventMetadata` ({@link CommandMetadataOptions}) accepts: a
1113
+ * function of the {@link Decision} returning one metadata entry per event
1114
+ * (`undefined` for an event without metadata), directly or as an `Effect`. The
1115
+ * effect's `E3` joins the handler's error channel and its `R3` the handler's
1116
+ * requirements.
923
1117
  *
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.
1118
+ * Function-only: the number of events is known only once `decide` has run.
1119
+ * The `Effect` form lets ids be minted through the `Crypto` service
1120
+ * (`crypto.randomULID`, or `randomUUIDv7`), which keeps them deterministic
1121
+ * under `TestClock` and a test `Crypto` layer with fixed bytes.
928
1122
  */
929
- export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never> {
1123
+ export type EventMetadataInput<State, Event, TMetadata, E3 = never, R3 = never> = (decision: Decision<State, Event>) => ReadonlyArray<TMetadata | undefined> | Effect.Effect<ReadonlyArray<TMetadata | undefined>, E3, R3>;
1124
+ /**
1125
+ * The form {@link CommandOptionsBase.eventTimestamps} accepts: a function of
1126
+ * the {@link Decision} returning one timestamp per event (`undefined` keeps
1127
+ * the append's `Clock` time), directly or as an `Effect` — exactly as
1128
+ * {@link EventMetadataInput}. The effect's `E4` joins the handler's error
1129
+ * channel and its `R4` the handler's requirements.
1130
+ *
1131
+ * Function-only: the number of events is known only once `decide` has run.
1132
+ * The `Effect` form lets times come from a service (a source log, a
1133
+ * per-tenant clock) or from `Clock`, which keeps them deterministic under
1134
+ * `TestClock`.
1135
+ *
1136
+ * `E4` / `R4` are separate from `eventMetadata`'s `E3` / `R3`: with one shared
1137
+ * pair, an `eventMetadata` effect and an `eventTimestamps` effect with
1138
+ * different error types would not compile — TypeScript fixes the parameter
1139
+ * from one candidate and rejects the other instead of inferring a union.
1140
+ */
1141
+ export type EventTimestampsInput<State, Event, E4 = never, R4 = never> = (decision: Decision<State, Event>) => ReadonlyArray<DateTime.DateTime | undefined> | Effect.Effect<ReadonlyArray<DateTime.DateTime | undefined>, E4, R4>;
1142
+ /**
1143
+ * The per-call metadata of a {@link commandHandler} handler: one value shared
1144
+ * by every event (`metadata`), or a function deriving one entry per event from
1145
+ * the decision (`eventMetadata`) — never both (see
1146
+ * {@link AppendMetadataOptions}).
1147
+ */
1148
+ export type CommandMetadataOptions<TMetadata, State, Event, E3 = never, R3 = never> = {
1149
+ /**
1150
+ * Metadata written onto every event the decision appends (see
1151
+ * {@link AppendMetadataOptions}). Fixed before `decide` runs; to give
1152
+ * each event its own entry, use `eventMetadata`.
1153
+ */
930
1154
  readonly metadata?: TMetadata;
1155
+ readonly eventMetadata?: undefined;
1156
+ } | {
1157
+ readonly metadata?: undefined;
1158
+ /**
1159
+ * Per-event metadata derived from the {@link Decision}: the function
1160
+ * returns one entry per event, in order, and entry `i` is written onto
1161
+ * event `i` (see {@link AppendMetadataOptions}).
1162
+ *
1163
+ * - Runs after `decide` and after the new events are folded, receiving
1164
+ * the same `Decision` a function-form `additionalItems` does — and
1165
+ * **before** `additionalItems`, so an effectful pair runs in a
1166
+ * predictable order.
1167
+ * - Not called when `decide` returns no events.
1168
+ * - Re-run on every attempt: each `VersionConflict` retry, and the
1169
+ * re-decision a stale unverified snapshot triggers
1170
+ * ({@link CommandHandlerOptions.verifySnapshot}`: false`). Ids minted
1171
+ * per attempt are safe — a failed attempt writes nothing.
1172
+ * - A result whose length differs from the decision's events fails with
1173
+ * `ValidationError` (`operation: "EventStore.commandHandler.eventMetadata"`),
1174
+ * never retried. A failure of the returned `Effect` surfaces as is,
1175
+ * and is not retried either.
1176
+ *
1177
+ * Without `idempotency`, a redelivery after a lost response decides and
1178
+ * appends again, minting new ids. With it, the redelivery fails with
1179
+ * `DuplicateCommand`, so the first commit's ids are the only ones written:
1180
+ *
1181
+ * ```ts
1182
+ * // `crypto` is the `Crypto.Crypto` service; a ULID takes its time from the
1183
+ * // `Clock`, so `TestClock` plus a fixed-bytes `Crypto` make it deterministic
1184
+ * const handle = EventStore.commandHandler(decider, stream, {
1185
+ * idempotency: { ttl: Duration.days(1) },
1186
+ * })
1187
+ * yield* handle(streamId, command, {
1188
+ * commandId: deliveryId,
1189
+ * eventMetadata: ({ events }) =>
1190
+ * Effect.forEach(events, () =>
1191
+ * Effect.map(crypto.randomULID, (eventId) => ({ eventId })),
1192
+ * ),
1193
+ * })
1194
+ * ```
1195
+ */
1196
+ readonly eventMetadata?: EventMetadataInput<State, Event, TMetadata, E3, R3>;
1197
+ };
1198
+ /**
1199
+ * The options of a {@link commandHandler} handler other than its metadata
1200
+ * (see {@link CommandMetadataOptions}).
1201
+ */
1202
+ export interface CommandOptionsBase<State = unknown, Event = unknown, E2 = never, R2 = never, E4 = never, R4 = never> {
931
1203
  /**
932
1204
  * Identifier for this command delivery. Required when the handler was created
933
1205
  * with `idempotency`; a replayed id fails with `DuplicateCommand` — also when
@@ -964,7 +1236,7 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
964
1236
  readonly expectedVersion?: number | undefined;
965
1237
  /**
966
1238
  * Caller-owned transact items committed atomically with the produced events
967
- * (see {@link AppendOptions.additionalItems}).
1239
+ * (see {@link AppendOptionsBase.additionalItems}).
968
1240
  *
969
1241
  * Either a static array, or a function of the {@link Decision} — an inline
970
1242
  * projection — returning the array or an `Effect` of it. The function runs
@@ -974,31 +1246,80 @@ export interface CommandOptions<TMetadata, State = unknown, Event = unknown, E2
974
1246
  * `AdditionalItemConditionFailed` refer to the array it returned.
975
1247
  */
976
1248
  readonly additionalItems?: AdditionalItemsInput<State, Event, E2, R2>;
1249
+ /**
1250
+ * Per-event timestamps derived from the {@link Decision}: the function
1251
+ * returns one entry per event, in order, directly or as an `Effect` (see
1252
+ * {@link EventTimestampsInput}), and entry `i` is written as event `i`'s
1253
+ * `timestamp` — `undefined` keeps the append's `Clock` time (see
1254
+ * {@link AppendOptionsBase.eventTimestamps}). Independent of `metadata` /
1255
+ * `eventMetadata`. The idempotency sentinel, its TTL and an inline snapshot
1256
+ * keep the append's `Clock` time; timestamps carry no ordering check.
1257
+ *
1258
+ * - Runs after `eventMetadata` and before `additionalItems`, each to
1259
+ * completion before the next, all receiving the same `Decision`; not
1260
+ * called when `decide` returns no events; re-run on every attempt — each
1261
+ * `VersionConflict` retry and the re-decision a stale unverified snapshot
1262
+ * triggers — as `eventMetadata` is.
1263
+ * - A failure of the returned `Effect` surfaces as is, is not retried, and
1264
+ * `additionalItems` is not run.
1265
+ * - A result — plain or produced by the `Effect` — of the wrong length, or
1266
+ * with an entry that is neither a valid `DateTime` (finite epoch) nor
1267
+ * `undefined`, fails with `ValidationError`
1268
+ * (`operation: "EventStore.commandHandler.eventTimestamps"`), never
1269
+ * retried. A value that is not a function is refused before anything is
1270
+ * read.
1271
+ *
1272
+ * ```ts
1273
+ * // plain: the times are in the events
1274
+ * eventTimestamps: ({ events }) => events.map((event) => event.occurredAt)
1275
+ *
1276
+ * // Effect: the times come from a service
1277
+ * eventTimestamps: ({ events }) =>
1278
+ * Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId))
1279
+ * ```
1280
+ */
1281
+ readonly eventTimestamps?: EventTimestampsInput<State, Event, E4, R4>;
977
1282
  }
1283
+ /**
1284
+ * Per-call options accepted by a handler produced by {@link commandHandler}:
1285
+ * {@link CommandOptionsBase} plus the exclusive `metadata` / `eventMetadata`
1286
+ * pair ({@link CommandMetadataOptions}).
1287
+ *
1288
+ * `State` / `Event` type the {@link Decision} a function-form `additionalItems`,
1289
+ * `eventMetadata` or `eventTimestamps` receives; `E2` / `R2` are the error and
1290
+ * requirements of the `Effect` `additionalItems` may return, `E3` / `R3` those
1291
+ * of the `Effect` `eventMetadata` may return, `E4` / `R4` those of the
1292
+ * `Effect` `eventTimestamps` may return — separate, so effects with different
1293
+ * error types all join the handler's error channel. The handler infers all of
1294
+ * them, so they only need naming when this type is written out by hand.
1295
+ */
1296
+ export type CommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptionsBase<State, Event, E2, R2, E4, R4> & CommandMetadataOptions<TMetadata, State, Event, E3, R3>;
978
1297
  /** 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> {
1298
+ export type IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown, E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never> = CommandOptions<TMetadata, State, Event, E2, R2, E3, R3, E4, R4> & {
980
1299
  readonly commandId: string;
981
- }
1300
+ };
982
1301
  /**
983
1302
  * Options arity: configuring `idempotency` makes the handler's options parameter
984
1303
  * required (and `commandId` within it non-optional), so a missing `commandId`
985
1304
  * is a compile error rather than a silent downgrade to at-least-once.
986
1305
  */
987
- type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2> = TConfig extends {
1306
+ type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2, E3, R3, E4, R4> = TConfig extends {
988
1307
  readonly idempotency: object;
989
- } ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2> | undefined];
1308
+ } ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4> | undefined];
990
1309
  type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
991
1310
  /**
992
1311
  * A handler over an {@link EventStream}. Generic per call over the error (`E2`)
993
1312
  * and requirements (`R2`) of a function-form `additionalItems` that returns an
994
- * `Effect`; both are `never` otherwise.
1313
+ * `Effect`, those (`E3` / `R3`) of an `eventMetadata` that does, and those
1314
+ * (`E4` / `R4`) of an `eventTimestamps` that does; all are `never` otherwise.
995
1315
  */
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>;
1316
+ type CommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, DynamoClient | TableConfig | R2 | R3 | R4>;
997
1317
  /**
998
1318
  * A handler over a {@link BoundEventStream}: the stream's services are already
999
- * provided, so a function-form `additionalItems`' `R2` is its only requirement.
1319
+ * provided, so the `R2` / `R3` / `R4` of a function-form `additionalItems` /
1320
+ * `eventMetadata` / `eventTimestamps` are its only requirements.
1000
1321
  */
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>;
1322
+ type BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TConfig extends CommandHandlerOptions | undefined = undefined> = <E2 = never, R2 = never, E3 = never, R3 = never, E4 = never, R4 = never>(streamId: StreamIdInput<TStreamIdFields>, command: Command, ...options: CommandOptionsArgs<TMetadata, TConfig, State, TEvent, E2, R2, E3, R3, E4, R4>) => Effect.Effect<CommandHandlerResult<State, TEvent>, CommandHandlerErrors<E> | E2 | E3 | E4, R2 | R3 | R4>;
1002
1323
  /**
1003
1324
  * The handler the data-last {@link commandHandler} returns for stream `S`: a
1004
1325
  * {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
@@ -1042,6 +1363,24 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1042
1363
  * yield* handle({ matchId: "m-1" }, command, {
1043
1364
  * additionalItems: ({ state }) => [Scoreboard.put({ matchId: "m-1", runs: state.runs })],
1044
1365
  * })
1366
+ *
1367
+ * // Per-event metadata derived from the decision: entry i is written onto event i
1368
+ * // (`crypto` is the `Crypto.Crypto` service; `randomUUIDv7` works the same way)
1369
+ * yield* handle({ matchId: "m-1" }, command, {
1370
+ * eventMetadata: ({ events }) =>
1371
+ * Effect.forEach(events, () => Effect.map(crypto.randomULID, (eventId) => ({ eventId }))),
1372
+ * })
1373
+ *
1374
+ * // Per-event timestamps derived from the decision (undefined → the Clock time)
1375
+ * yield* handle({ matchId: "m-1" }, command, {
1376
+ * eventTimestamps: ({ events }) => events.map((event) => event.occurredAt),
1377
+ * })
1378
+ *
1379
+ * // ...or from a service, as an Effect (its errors / requirements join the handler's)
1380
+ * yield* handle({ matchId: "m-1" }, command, {
1381
+ * eventTimestamps: ({ events }) =>
1382
+ * Effect.forEach(events, (event) => SourceLog.originalTime(event.sourceId)),
1383
+ * })
1045
1384
  * ```
1046
1385
  *
1047
1386
  * Each invocation runs
@@ -1057,7 +1396,7 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1057
1396
  * item alone (one `GetItem`); an answer that is not a successful append is
1058
1397
  * confirmed against the head first, and a snapshot that proves stale falls
1059
1398
  * back to the verified load — in which case `decide` may run twice.
1060
- * - **expectedVersion.** See {@link CommandOptions.expectedVersion}.
1399
+ * - **expectedVersion.** See {@link CommandOptionsBase.expectedVersion}.
1061
1400
  * - **Fold before append.** The new events are folded into state before the
1062
1401
  * append, so the state returned, snapshotted and handed to a function-form
1063
1402
  * `additionalItems` (see {@link Decision}) is always the `evolve` fold.
@@ -1067,8 +1406,8 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1067
1406
  * fresh snapshot is written (best-effort) after a successful append once the
1068
1407
  * cadence threshold is crossed. See {@link SnapshotMode}.
1069
1408
  * - **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
1409
+ * too large for one transaction is rejected whole by DynamoDB and fails with
1410
+ * `AppendTooLarge` — nothing is written. A large command is split by the application into stepped
1072
1411
  * commands — each an ordinary call, chained by `expectedVersion` — so a
1073
1412
  * failure partway leaves the stream at a real intermediate state.
1074
1413
  *
@@ -1087,9 +1426,13 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1087
1426
  * `EventStreamTypeId` brand of the second argument.
1088
1427
  */
1089
1428
  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>;
1429
+ <State, Command, TEvent, E, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig): <S extends BoundEventStream<TEvent, any, any, State, any, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
1430
+ <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1431
+ readonly [K in TIndexName]: StreamIndexProjection;
1432
+ }, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
1433
+ <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1434
+ readonly [K in TIndexName]: StreamIndexProjection;
1435
+ }, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
1093
1436
  };
1094
1437
  /**
1095
1438
  * Reconstruct state from events by folding through a decider's `evolve` function.