effect-dynamodb 1.31.0 → 1.33.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.
@@ -11,7 +11,15 @@
11
11
  * - `commandHandler` combinator for the load-decide-fold-append cycle
12
12
  * (snapshot-aware single-request loads, consistent loads, an optional
13
13
  * `VersionConflict` retry policy, caller-supplied `expectedVersion`,
14
- * decision-derived `additionalItems`, inline snapshots)
14
+ * decision-derived `additionalItems`, inline snapshots, a caller's own
15
+ * state record as the load via `loadState`)
16
+ * - Head proofs (`append({ headProof })`, #164): an additional item or the
17
+ * inline snapshot proves the stream head in place of the version-contiguity
18
+ * `ConditionCheck`
19
+ * - Sealing (#166): `append(..., { seal: true })` makes its last event the
20
+ * stream's final one (`StreamSealed` for any later append), and
21
+ * `stream.appendOp(...)` carries one stream's append — sealing or not — in
22
+ * another append's transaction
15
23
  * - Stream indexes (`makeStream({ indexes })`): sub-streams ordered by a key
16
24
  * derived from each event, on an LSI or GSI, each projecting all of the
17
25
  * event, all but its metadata, or only its keys — `readIndex`,
@@ -36,13 +44,13 @@
36
44
  import type { AttributeDefinition, GlobalSecondaryIndex, LocalSecondaryIndex } from "@aws-sdk/client-dynamodb";
37
45
  import { type PackedConfig } from "@effect-dynamodb/schema/DynamoModel.js";
38
46
  import * as DynamoSchema from "@effect-dynamodb/schema/DynamoSchema.js";
39
- import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
47
+ import { AdditionalItemConditionFailed, AppendTooLarge, type ConcurrentModification, DuplicateCommand, type OptimisticLockError, StreamSealed, TransactionCancelled, type UniqueConstraintViolation, ValidationError, VersionConflict } from "@effect-dynamodb/schema/Errors.js";
40
48
  import { DateTime, type Duration, Effect, Option, Pipeable, Schedule, Schema } from "effect";
41
49
  import { DynamoClient, type DynamoClientError } from "./DynamoClient.js";
42
50
  import { type EventProjection } from "./internal/EventProjection.js";
43
51
  import { type MultiProjection, type MultiSource } from "./internal/MultiProjection.js";
44
52
  import { type StreamImage } from "./internal/Packed.js";
45
- import { type TransactWriteOp } from "./internal/TransactWriteOps.js";
53
+ import { type AppendWriteOp, type StreamAppendOp } from "./internal/TransactWriteOps.js";
46
54
  import * as Query from "./Query.js";
47
55
  import { type Table, type TableConfig } from "./Table.js";
48
56
  /**
@@ -81,6 +89,18 @@ export interface StreamEvent<A, M = Record<string, unknown> | undefined> {
81
89
  * ({@link AppendOptionsBase.eventTimestamps}).
82
90
  */
83
91
  readonly timestamp: string;
92
+ /**
93
+ * Present, and `true`, only on a sealing event: the stream's final event
94
+ * (see {@link AppendOptionsBase.seal}). Absent on every other event.
95
+ *
96
+ * Read from the item's `__edd_sealed__` marker, so it is reported by every
97
+ * read of the item itself — `read`, `readFrom`, `query.events`,
98
+ * `readLatest`, `decodeStreamImage`, and a stream index of projection
99
+ * `"all"` or `"keys"` — but not through a `"withoutMetadata"` index (whose
100
+ * projection does not include the marker), nor on a raw row whose
101
+ * `Query.select` projection omits it.
102
+ */
103
+ readonly sealed?: true;
84
104
  }
85
105
  /**
86
106
  * A {@link StreamEvent} together with the event's stored `data` in its wire
@@ -219,7 +239,8 @@ export interface AppendMeasurement {
219
239
  * What the stream's own actions (the contiguity check, the event puts, the
220
240
  * sentinel and the inline snapshot) add to the `TransactWriteItems` request
221
241
  * — its 4 MB (4,194,304-byte) limit counts items, keys and expressions.
222
- * Caller `additionalItems` are not included: add their size yourself.
242
+ * Caller `additionalItems` are not included: add their size yourself. With
243
+ * a `headProof` there is no contiguity check to count.
223
244
  */
224
245
  readonly transactionBytes: number;
225
246
  }
@@ -316,6 +337,20 @@ export type AppendMetadataOptions<TMetadata> = {
316
337
  */
317
338
  readonly eventMetadata?: ReadonlyArray<TMetadata | undefined>;
318
339
  };
340
+ /**
341
+ * The item of an {@link EventStream.append} that proves the stream head is at
342
+ * `expectedVersion`, in place of the version-contiguity `ConditionCheck` (see
343
+ * {@link AppendOptionsBase.headProof}).
344
+ *
345
+ * - `{ additionalItem: n }` — `additionalItems[n]`, whose own condition holds
346
+ * only when the stream is at `expectedVersion` (the caller's state record,
347
+ * conditioned on the version it was loaded at, or on its absence at 0).
348
+ * - `"snapshot"` — the append's inline snapshot, conditioned on
349
+ * `asOfVersion = expectedVersion`.
350
+ */
351
+ export type HeadProof = {
352
+ readonly additionalItem: number;
353
+ } | "snapshot";
319
354
  /**
320
355
  * The options of {@link EventStream.append} other than its metadata (see
321
356
  * {@link AppendMetadataOptions}).
@@ -341,8 +376,21 @@ export interface AppendOptionsBase<TState = never> {
341
376
  * the entity's put would: `UniqueConstraintViolation`, a history
342
377
  * `ValidationError`, or — a race lost on every attempt —
343
378
  * `OptimisticLockError` / `ConcurrentModification`.
379
+ *
380
+ * **Another stream's append** (#166): `otherStream.appendOp(...)` (see
381
+ * {@link EventStream.appendOp}) rides here too, so several streams' events
382
+ * commit together or not at all — and a stream can be sealed in the same
383
+ * transaction that opens its successor. Its failures name the inner stream
384
+ * (`StreamSealed`, `VersionConflict`), judged after this stream's own and
385
+ * ahead of the other items' errors.
386
+ *
387
+ * The element type is {@link AppendWriteOp}, wider than the
388
+ * `TransactWriteOp` that `Transaction.transactWrite` takes. Passing a
389
+ * `TransactWriteOp[]` here is unaffected; code that reads this property's
390
+ * type and forwards the value to `transactWrite` must annotate it
391
+ * `ReadonlyArray<TransactWriteOp>`.
344
392
  */
345
- readonly additionalItems?: ReadonlyArray<TransactWriteOp>;
393
+ readonly additionalItems?: ReadonlyArray<AppendWriteOp>;
346
394
  /** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
347
395
  readonly idempotency?: AppendIdempotency;
348
396
  /**
@@ -380,6 +428,107 @@ export interface AppendOptionsBase<TState = never> {
380
428
  * repeat, run backwards or lie in the future, as imported history can.
381
429
  */
382
430
  readonly eventTimestamps?: ReadonlyArray<DateTime.DateTime | undefined>;
431
+ /**
432
+ * The item that proves the stream head is at `expectedVersion`, in place of
433
+ * the version-contiguity `ConditionCheck`. Omitted: the check is written.
434
+ *
435
+ * The check reads the event at `expectedVersion`, and DynamoDB bills a
436
+ * `ConditionCheck` inside a transaction at 2 WCU per KB of the checked
437
+ * item, so with large events it is a sizeable part of every append. The
438
+ * event puts already reject a stale `expectedVersion`; the check rejects
439
+ * one that is ahead of the head, which would leave a gap. An item whose own
440
+ * condition holds only at `expectedVersion` proves that just as well.
441
+ *
442
+ * - `{ additionalItem: n }` — `additionalItems[n]` is the proof. It must
443
+ * carry a condition of its own (`.condition()`, `create()`,
444
+ * `Transaction.check`); one without, or an `n` that is not an index into
445
+ * `additionalItems`, fails with `ValidationError` before anything is
446
+ * written. When that condition fails, the append fails with
447
+ * `VersionConflict` — never `AdditionalItemConditionFailed` — judged
448
+ * after `DuplicateCommand` and the event puts, ahead of the other items.
449
+ *
450
+ * **The caller owns the invariant**: every writer of the stream (a
451
+ * handler, an import, a migration, a raw `append`) must write the item in
452
+ * the same transaction as its events, conditioned the same way. An append
453
+ * without it leaves the item behind the head, and every later append
454
+ * fails with `VersionConflict` until it is repaired. Writing the item's
455
+ * version without the events it counts moves it ahead of the head — the
456
+ * one way to open a gap — so write that version only in their append.
457
+ * - `"snapshot"` — the inline snapshot is the proof: its `Put` is
458
+ * conditioned on `asOfVersion = expectedVersion` (`attribute_not_exists`
459
+ * at version 0), and a failure is a `VersionConflict`. Requires
460
+ * `snapshot` (else `ValidationError`) and a stream whose snapshot config
461
+ * is `mode: "inline"` without `every` (else a defect, `[EDD-9085]`). A
462
+ * snapshot can lag the head (a raw `append`, data written before inline
463
+ * mode), and then fails the condition: use it only when the snapshot is
464
+ * known to be at the head, as {@link CommandHandlerOptions.headProof}
465
+ * does.
466
+ *
467
+ * **On a sealed stream** ({@link AppendOptionsBase.seal}): a head proof
468
+ * replaces the contiguity check, which is where the seal is enforced.
469
+ *
470
+ * - `"snapshot"`: the sealing append's inline snapshot carries the seal
471
+ * marker, and the proof's condition also requires the snapshot to be
472
+ * unmarked (`StreamSealed` otherwise). A seal written through a
473
+ * definition **without** this snapshot config leaves the snapshot
474
+ * unmarked below the seal; a later `writeSnapshot` at the seal version
475
+ * then writes an unmarked snapshot there, and a snapshot-proof append can
476
+ * land past the seal (see {@link EventStream.writeSnapshot}). Seal through
477
+ * a definition that shares the writers' snapshot config.
478
+ * - `{ additionalItem: n }`: the library does not enforce the seal (it
479
+ * cannot know the stream is sealed without a read). Seal such a stream
480
+ * with **exactly one event**, and keep the record at the seal's
481
+ * `expectedVersion` (one below the seal, with a sealed flag and the
482
+ * seal's version for {@link LoadedState.sealedVersion}): every
483
+ * record-proof writer then appends at that version, and its first event
484
+ * put lands on the sealing event and fails with `StreamSealed`.
485
+ * Advancing the record to the seal's version lets a record-proof writer
486
+ * append past the seal, unless every writer's record condition also tests
487
+ * the sealed flag. A seal of several events leaves a record-proof writer
488
+ * on `VersionConflict` instead.
489
+ */
490
+ readonly headProof?: HeadProof | undefined;
491
+ /**
492
+ * Seal the stream with this append (#166): its **last** event becomes the
493
+ * stream's final event, and no append can land after it — one that tries
494
+ * fails with `StreamSealed`. `false` and `undefined` do nothing.
495
+ *
496
+ * The append writes events `expectedVersion + 1 … expectedVersion + n`, so
497
+ * the seal version is `expectedVersion + n`. The library writes a marker
498
+ * attribute (`__edd_sealed__`) on that event's item, and on the inline
499
+ * snapshot when the append writes one; reads report it
500
+ * ({@link StreamEvent.sealed}, {@link LatestState.seal}). A seal is
501
+ * permanent.
502
+ *
503
+ * - A value that is not a boolean, or `true` with no events, fails with
504
+ * `ValidationError` (`operation: "EventStore.append.seal"`) before
505
+ * anything is written.
506
+ * - On a stream whose snapshot config is `mode: "inline"` without `every`,
507
+ * `true` also requires `snapshot`, so the snapshot at the seal version is
508
+ * the marked one and an unverified load sees the seal.
509
+ * - Combines with every other option. Through another stream's
510
+ * `additionalItems` use {@link EventStream.appendOp}.
511
+ *
512
+ * **Enforcement.** Every append after version 0 already checks the event
513
+ * at its `expectedVersion`; that check also requires the event to be
514
+ * unmarked. A writer that loaded after the seal fails it; one that loaded
515
+ * just before fails on its first event put, which lands on the sealing
516
+ * event; both get `StreamSealed`. A writer further behind gets
517
+ * `VersionConflict` and learns of the seal when it reloads.
518
+ *
519
+ * **Upgrade every writer first.** A writer on a library version that does
520
+ * not enforce the seal, a record-proof writer whose record was advanced to
521
+ * the seal, or a snapshot-proof writer on an unmarked snapshot at the seal
522
+ * version can land one event past the seal — and that event, unmarked,
523
+ * reopens the stream for every writer. Reads still report the seal while
524
+ * the sealing event is among the items they fetch
525
+ * (`seal.version < version` on {@link LatestState}).
526
+ *
527
+ * Zero-event appends (only `additionalItems` and/or `idempotency`) write no
528
+ * event, and still land on a sealed stream: a seal stops events, not side
529
+ * writes.
530
+ */
531
+ readonly seal?: boolean | undefined;
383
532
  }
384
533
  /**
385
534
  * Options accepted by {@link EventStream.append}: {@link AppendOptionsBase}
@@ -390,8 +539,22 @@ export interface AppendOptionsBase<TState = never> {
390
539
  * union: an interface cannot extend it.
391
540
  */
392
541
  export type AppendOptions<TMetadata, TState = never> = AppendOptionsBase<TState> & AppendMetadataOptions<TMetadata>;
393
- /** Error channel of {@link EventStream.append}. */
394
- export type AppendError = VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
542
+ /**
543
+ * The options of {@link EventStream.appendOp}: those of
544
+ * {@link EventStream.append} except `additionalItems`, `idempotency` and
545
+ * `headProof`, which a nested append may not carry (they are refused with
546
+ * `ValidationError` at runtime too, `operation: "EventStore.appendOp"`).
547
+ * The appending stream carries them: a record the nested stream's writers
548
+ * load goes in the OUTER append's `additionalItems`.
549
+ */
550
+ export type NestedAppendOptions<TMetadata, TState = never> = Omit<AppendOptionsBase<TState>, "additionalItems" | "idempotency" | "headProof"> & AppendMetadataOptions<TMetadata>;
551
+ export type { AppendWriteOp, StreamAppendOp };
552
+ /**
553
+ * Error channel of {@link EventStream.append}. A nested
554
+ * {@link EventStream.appendOp}'s failures are reported through it too, naming
555
+ * the inner stream.
556
+ */
557
+ export type AppendError = VersionConflict | StreamSealed | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
395
558
  /**
396
559
  * Options accepted by {@link EventStream.read}, {@link EventStream.readFrom} and
397
560
  * {@link EventStream.currentVersion}.
@@ -455,11 +618,30 @@ export interface ReadLatestOptions extends ReadOptions {
455
618
  * present, it is the snapshot's `asOfVersion` and `events` is empty —
456
619
  * **unverified**: events appended without a snapshot (a raw `append`, data
457
620
  * written before `mode: "inline"`) may follow it.
621
+ * - `seal` — the sealing event, when the read found one (see
622
+ * {@link AppendOptionsBase.seal}).
458
623
  */
459
624
  export interface LatestState<TState, TEvent, M = Record<string, unknown> | undefined> {
460
625
  readonly snapshot: Option.Option<Snapshot<TState>>;
461
626
  readonly events: ReadonlyArray<StreamEvent<TEvent, M>>;
462
627
  readonly version: number;
628
+ /**
629
+ * The sealing event when the read found one, absent otherwise. It is
630
+ * separate from `events` because `events` holds only what follows the
631
+ * snapshot, and a snapshot written by the sealing append already folds it
632
+ * in (it is in `events` too when it follows the snapshot).
633
+ *
634
+ * Found from **any** sealed event among the items the read fetches, not
635
+ * only the newest, with no extra request — except with
636
+ * `verifySnapshot: false`, where a sealed snapshot costs one `GetItem` of
637
+ * the sealing event. An unverified read of a snapshot left unmarked (a seal
638
+ * written through a definition without this snapshot config) does not
639
+ * report it.
640
+ *
641
+ * `seal.version < version` means events were written past the seal by a
642
+ * writer that does not enforce it: they are on a closed stream.
643
+ */
644
+ readonly seal?: StreamEvent<TEvent, M>;
463
645
  }
464
646
  /**
465
647
  * The stream id as a stream's index functions receive it: the value of each
@@ -883,6 +1065,23 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
883
1065
  * infers the `pipe` subject. Method syntax makes the parameter bivariant, so
884
1066
  * `EventStream<E, F, M, never>` unifies with `EventStream<E, F, M, TState>`
885
1067
  * again. Callers are unaffected: supplying a `state` still requires `TState`.
1068
+ *
1069
+ * **Sealed streams** ({@link AppendOptionsBase.seal}): `writeSnapshot` is
1070
+ * one plain conditional `PutItem`. It does not know whether the stream is
1071
+ * sealed and never writes the seal marker, so it **must not target a sealed
1072
+ * head with an unmarked snapshot**. Skip sealed streams in a job that
1073
+ * snapshots streams (`readLatest(...).seal` is present), or seal through a
1074
+ * definition that shares the writers' snapshot config: the sealing append
1075
+ * then writes a marked snapshot at the seal version, which this monotonic
1076
+ * write never replaces.
1077
+ *
1078
+ * The residual when it does: a stream sealed at `S` through a definition
1079
+ * without the writers' `mode: "inline"`-without-`every` snapshot config
1080
+ * keeps an unmarked snapshot below `S`; a `writeSnapshot` at `S` then puts
1081
+ * an unmarked snapshot at the seal version, and an append with
1082
+ * `headProof: "snapshot"` at `S` passes its proof and lands event `S + 1`
1083
+ * past the seal — which reopens the stream for every writer. A verified
1084
+ * `readLatest` still reports the seal (`seal.version < version`).
886
1085
  */
887
1086
  writeSnapshot(streamId: StreamIdInput<TStreamIdFields>, state: TState, asOfVersion: number): Effect.Effect<void, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
888
1087
  /**
@@ -900,6 +1099,55 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
900
1099
  * `options.snapshot` is `never`) stays assignable to other streams.
901
1100
  */
902
1101
  append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, DynamoClient | TableConfig>;
1102
+ /**
1103
+ * An append of this stream as a transact item of ANOTHER stream's append
1104
+ * (its `additionalItems`, or a `commandHandler`'s): it commits if and only
1105
+ * if that append does (#166). Nothing is built or written until the outer
1106
+ * append runs.
1107
+ *
1108
+ * - **With or without `seal`.** Without it, several streams' events commit
1109
+ * together or not at all (an atomic multi-stream append). With it, a
1110
+ * stream is sealed in the same transaction that opens its successor:
1111
+ *
1112
+ * ```ts
1113
+ * const latest = yield* Ledgers.readLatest(genA, { consistentRead: true })
1114
+ * yield* Ledgers.append(genB, [new LedgerOpened({ continuesFrom: latest.version })], 0, {
1115
+ * additionalItems: [
1116
+ * Ledgers.appendOp(genA, [new LedgerClosed({ continuedIn: "4" })], latest.version, {
1117
+ * seal: true,
1118
+ * }),
1119
+ * ],
1120
+ * })
1121
+ * ```
1122
+ * - **The seal's `expectedVersion` is the version the carried-over state
1123
+ * was read at** — the same `readLatest` result, or the version carried in
1124
+ * the decided opening event — never a separate read of the stream's head.
1125
+ * Re-reading the head between the carry-over read and the commit seals
1126
+ * over an append the opening event never carried, and that append is
1127
+ * lost. With the carry-over version, a stale append in between fails the
1128
+ * seal's check instead (`VersionConflict` naming this stream): re-read and
1129
+ * build both again.
1130
+ * - It is a full append of this stream — its contiguity check, event puts,
1131
+ * seal marker and optional inline snapshot (`seal`, `snapshot`,
1132
+ * `metadata` / `eventMetadata`, `eventTimestamps`) — enforced
1133
+ * independently of the outer append. It may not carry `additionalItems`,
1134
+ * `idempotency` or `headProof` (see {@link NestedAppendOptions}); a record
1135
+ * this stream's writers load (a `loadState` record, a record head proof)
1136
+ * is written by the outer append's `additionalItems`, beside this op —
1137
+ * on a sealing op with `sealedVersion` (see {@link LoadedState}). Zero
1138
+ * events fail with `ValidationError` (`operation: "EventStore.appendOp"`).
1139
+ * - Its failures name this stream: `StreamSealed` (terminal) or
1140
+ * `VersionConflict`, judged after the outer stream's own and ahead of
1141
+ * the other additional items. It cannot be a `headProof` target.
1142
+ * - **One client per append.** Its items are sent in the outer append's
1143
+ * single `TransactWriteItems`, through the outer append's `DynamoClient`.
1144
+ * This unbound op resolves this stream's `TableConfig` from the outer
1145
+ * append's context, which must provide it; a bound stream's op resolves
1146
+ * it from its own captured context — but never uses its captured client.
1147
+ * Every table must be reachable through the outer client.
1148
+ * - `Transaction.transactWrite` does not accept it.
1149
+ */
1150
+ appendOp(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: NestedAppendOptions<TMetadata, TState> | undefined): StreamAppendOp;
903
1151
  /**
904
1152
  * Read every event of the stream, ascending by version. Pass
905
1153
  * `{ consistentRead: true }` for read-your-writes — see {@link ReadOptions}.
@@ -943,6 +1191,10 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
943
1191
  * verified read. Accepted only on a `mode: "inline"` stream without `every`
944
1192
  * (otherwise a defect, `[EDD-9068]`), ignored on a stream without a
945
1193
  * `snapshot` config — see {@link ReadLatestOptions}.
1194
+ *
1195
+ * On a sealed stream the result carries the sealing event as `seal` when
1196
+ * the read fetches the sealing event, in any mode (see
1197
+ * {@link LatestState.seal} for the cases it does not).
946
1198
  */
947
1199
  readLatest(streamId: StreamIdInput<TStreamIdFields>, options?: ReadLatestOptions | undefined): Effect.Effect<LatestState<TState, TEvent, StreamMetadata<TMetadata>>, DynamoClientError | ValidationError, DynamoClient | TableConfig>;
948
1200
  /**
@@ -1191,8 +1443,8 @@ export interface EventStream<TEvent, TStreamIdFields extends ReadonlyArray<strin
1191
1443
  * declare (such as a misspelled `projection`, also a type error).
1192
1444
  * @throws `[EDD-9064]` when an index attribute is one the stream writes itself
1193
1445
  * (`pk`, `sk`, `__edd_e__`, `streamId`, `version`, `eventType`, `data`,
1194
- * `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, or the
1195
- * default TTL attribute `_ttl`). A custom `TableConfig.ttlAttributeName`
1446
+ * `metadata`, `timestamp`, `asOfVersion`, `state`, `commandId`, the seal
1447
+ * marker `__edd_sealed__`, or the default TTL attribute `_ttl`). A custom `TableConfig.ttlAttributeName`
1196
1448
  * is only known at runtime: an index attribute equal to it fails each append
1197
1449
  * with `ValidationError`.
1198
1450
  * @throws `[EDD-9065]` when two indexes share a physical index name or an
@@ -1272,6 +1524,15 @@ export interface BoundEventStream<TEvent, TStreamIdFields extends ReadonlyArray<
1272
1524
  readSnapshot(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<Option.Option<Snapshot<TState>>, DynamoClientError | ValidationError, never>;
1273
1525
  /** See {@link EventStream.append}. */
1274
1526
  append(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: AppendOptions<TMetadata, TState> | undefined): Effect.Effect<AppendResult<TEvent>, AppendError, never>;
1527
+ /**
1528
+ * See {@link EventStream.appendOp}. The op builds in this bound stream's
1529
+ * captured context, so it resolves this stream's `TableConfig` (and
1530
+ * `Compression` registry) itself: a stream on another table needs no extra
1531
+ * layer in the outer append. Its items are still sent through the OUTER
1532
+ * append's `DynamoClient` — one transaction cannot span clients — so the
1533
+ * table must be reachable through it.
1534
+ */
1535
+ appendOp(streamId: StreamIdInput<TStreamIdFields>, events: ReadonlyArray<TEvent>, expectedVersion: number, options?: NestedAppendOptions<TMetadata, TState> | undefined): StreamAppendOp;
1275
1536
  /** See {@link EventStream.read}. */
1276
1537
  read(streamId: StreamIdInput<TStreamIdFields>, options?: ReadOptions | undefined): Effect.Effect<ReadonlyArray<StreamEvent<TEvent, StreamMetadata<TMetadata>>>, DynamoClientError | ValidationError, never>;
1277
1538
  /** See {@link EventStream.readFrom}. */
@@ -1406,14 +1667,43 @@ export declare const indexDefinitions: (...streams: ReadonlyArray<{
1406
1667
  };
1407
1668
  };
1408
1669
  }>) => StreamIndexDefinitions;
1670
+ /**
1671
+ * The state and stream version a {@link CommandHandlerOptions.loadState}
1672
+ * loads from the caller's own record: `version` is the stream version the
1673
+ * state is as of, which the handler appends after.
1674
+ */
1675
+ export interface LoadedState<State> {
1676
+ readonly state: State;
1677
+ readonly version: number;
1678
+ /**
1679
+ * The caller's record says the stream is sealed at this version (#166):
1680
+ * the handler then fails with `StreamSealed` before `decide`, without
1681
+ * spending a transaction (after `DuplicateCommand` for a committed
1682
+ * redelivery). It may be `version + 1`: a record-proof stream keeps its
1683
+ * record one below the seal (see {@link AppendOptionsBase.headProof}).
1684
+ *
1685
+ * Must be a non-negative integer, not below `version` — otherwise the call
1686
+ * fails with `ValidationError`
1687
+ * (`operation: "EventStore.commandHandler.loadState"`). The sealing
1688
+ * transaction writes it: in a changeover, the outer append's
1689
+ * `additionalItems` carries the record beside the sealing `appendOp`.
1690
+ * Without it, a record more than one version below the seal leaves the
1691
+ * handler on `VersionConflict` until its retry schedule ends.
1692
+ */
1693
+ readonly sealedVersion?: number | undefined;
1694
+ }
1409
1695
  /**
1410
1696
  * Handler-level configuration for {@link commandHandler}.
1411
1697
  *
1412
1698
  * `idempotency` carries the policy that is fixed for the handler; the
1413
1699
  * `commandId` that identifies one delivery can only be per-call and lives in the
1414
1700
  * handler's own options.
1701
+ *
1702
+ * `State` and `TStreamId` type {@link loadState}: the decider's state, and the
1703
+ * stream id the handler is called with (`any` for a data-last handler, whose
1704
+ * stream is not yet known).
1415
1705
  */
1416
- export interface CommandHandlerOptions {
1706
+ export interface CommandHandlerOptions<State = any, TStreamId = any> {
1417
1707
  /**
1418
1708
  * Retry policy applied to `VersionConflict` **only**.
1419
1709
  *
@@ -1427,9 +1717,16 @@ export interface CommandHandlerOptions {
1427
1717
  * A number `n` is shorthand for `Schedule.recurs(n)` (n retries *after* the
1428
1718
  * initial attempt). Omit for the default: no retry.
1429
1719
  *
1430
- * `DuplicateCommand` is deliberately NOT retried — it is terminal. Nor is
1431
- * any call that supplies {@link CommandOptionsBase.expectedVersion}: the caller asked
1432
- * for a conditional write, so its `VersionConflict` is always surfaced.
1720
+ * `DuplicateCommand` is deliberately NOT retried — it is terminal, as is
1721
+ * `StreamSealed` (#166). Nor is a `VersionConflict` of this stream on a
1722
+ * call that supplies {@link CommandOptionsBase.expectedVersion}: the caller
1723
+ * asked for a conditional write, so it is always surfaced.
1724
+ *
1725
+ * A `VersionConflict` naming **another** stream — a stream's `appendOp`
1726
+ * carried in `additionalItems` (#166) — is retried, with or without an
1727
+ * `expectedVersion`: this stream's If-Match was not wrong, and the retried
1728
+ * attempt reloads this stream and checks the If-Match again before
1729
+ * `decide`. Without a schedule it is returned as-is, naming that stream.
1433
1730
  */
1434
1731
  readonly retry?: number | Schedule.Schedule<unknown, VersionConflict> | undefined;
1435
1732
  /** Opt in to exactly-once command processing — see {@link AppendIdempotency}. */
@@ -1512,7 +1809,72 @@ export interface CommandHandlerOptions {
1512
1809
  * not extra item reads.
1513
1810
  */
1514
1811
  readonly verifySnapshot?: boolean | undefined;
1812
+ /**
1813
+ * Load the state and head from the caller's own record instead of the
1814
+ * stream (#164). `None` is a new stream: the decider's initial state at
1815
+ * version 0.
1816
+ *
1817
+ * The rest of the cycle is unchanged: the If-Match check against the loaded
1818
+ * `version`, `decide`, `evolve`, the derived items, then the append after
1819
+ * `version` — where the caller writes the record, through `additionalItems`.
1820
+ * A `VersionConflict` re-runs `loadState` under the {@link retry} policy, as
1821
+ * a stream load is re-run. A no-op decision or a `decide` failure is
1822
+ * answered from the record: the stream head is not read.
1823
+ *
1824
+ * - **Read strongly consistent.** An eventually consistent read can return
1825
+ * a record behind the head; the append then fails with `VersionConflict`
1826
+ * and burns a retry (nothing is corrupted).
1827
+ * - Without {@link headProof} the append still writes the
1828
+ * version-contiguity check, so a record that is stale or ahead of the
1829
+ * head conflicts rather than opening a gap. With
1830
+ * `headProof: { additionalItem: n }`, the record is the proof, and its
1831
+ * contract (see {@link AppendOptionsBase.headProof}) covers **every**
1832
+ * writer of the stream.
1833
+ * - A `version` that is not a non-negative integer, or a result that is not
1834
+ * an `Option`, fails with `ValidationError`
1835
+ * (`operation: "EventStore.commandHandler.loadState"`), never retried. A
1836
+ * failure of the `Effect` surfaces as is. Its error and requirements join
1837
+ * the handler's (a `BoundEventStream` handler's only requirements).
1838
+ * - On a snapshot-configured stream the snapshot is not read, so its
1839
+ * version is unknown: a `mode: "inline"` stream without `every` still
1840
+ * writes it in every append; with `every` (inline or after-append), one
1841
+ * is written when an append crosses a multiple of `every`.
1842
+ * - Refused with {@link verifySnapshot}`: false` (there is no stream load
1843
+ * to skip) and with `headProof: "snapshot"` (the handler never sees the
1844
+ * snapshot, so cannot know it is at the head): `[EDD-9085]` when the
1845
+ * handler is created.
1846
+ */
1847
+ readonly loadState?: ((streamId: TStreamId) => Effect.Effect<Option.Option<LoadedState<State>>, any, any>) | undefined;
1848
+ /**
1849
+ * The item that proves the stream head, in place of the version-contiguity
1850
+ * `ConditionCheck`, on every append the handler makes (see
1851
+ * {@link AppendOptionsBase.headProof}). Omitted: the check is written.
1852
+ *
1853
+ * - `{ additionalItem: n }` — the caller's record at `additionalItems[n]`,
1854
+ * conditioned on the loaded version (or its absence at version 0). Every
1855
+ * call whose decision appends must then return that item, carrying its
1856
+ * own condition; otherwise the call fails with `ValidationError` and
1857
+ * nothing is written. A failure of its condition is a `VersionConflict`,
1858
+ * and follows the {@link retry} policy. Usually paired with
1859
+ * {@link loadState}.
1860
+ * - `"snapshot"` — the inline snapshot, on a `mode: "inline"` stream
1861
+ * without `every` (any other throws `[EDD-9085]` when the handler is
1862
+ * created). Applied on an attempt only when its load found the snapshot
1863
+ * at the head — a verified load with no event after the snapshot, or an
1864
+ * unverified one ({@link verifySnapshot}`: false`); otherwise that
1865
+ * append writes the check. A conflict goes through the same stale-snapshot
1866
+ * handling and retry policy as without it.
1867
+ */
1868
+ readonly headProof?: HeadProof | undefined;
1515
1869
  }
1870
+ /** @internal The error of a handler config's {@link CommandHandlerOptions.loadState}. */
1871
+ type LoadStateError<TConfig> = TConfig extends {
1872
+ readonly loadState: (...args: any) => Effect.Effect<any, infer EL, any>;
1873
+ } ? EL : never;
1874
+ /** @internal The requirements of a handler config's {@link CommandHandlerOptions.loadState}. */
1875
+ type LoadStateContext<TConfig> = TConfig extends {
1876
+ readonly loadState: (...args: any) => Effect.Effect<any, any, infer RL>;
1877
+ } ? RL : never;
1516
1878
  /**
1517
1879
  * The outcome of one `decide` call, handed to a function-form
1518
1880
  * {@link CommandOptionsBase.additionalItems}, to `eventMetadata`
@@ -1543,8 +1905,23 @@ export interface Decision<State, Event> {
1543
1905
  * directly (pure projection) or as an `Effect` (a projection that needs a read).
1544
1906
  * The effect's `E2` joins the handler's error channel and its `R2` the handler's
1545
1907
  * requirements.
1908
+ *
1909
+ * The ops are {@link AppendWriteOp}s: another stream's `appendOp` (#166) rides
1910
+ * here too, so a handler on stream B can carry stream A's sealing append. Its
1911
+ * `expectedVersion` must come from the **decision** — the version carried in
1912
+ * the decided opening event (`decision.events[0].continuesFrom.version`, say),
1913
+ * from A's state as `decide` read it — never from a separate read of A's head,
1914
+ * which would seal over an append the opening event never carried.
1915
+ *
1916
+ * A `VersionConflict` naming A (not this handler's stream) is never taken
1917
+ * for this stream's lagging snapshot, and is retried by the
1918
+ * {@link CommandHandlerOptions.retry} schedule even under an If-Match. A
1919
+ * `StreamSealed` naming A is terminal: another closer sealed it.
1920
+ *
1921
+ * Code that reads this type and forwards a static array to
1922
+ * `Transaction.transactWrite` must annotate it `ReadonlyArray<TransactWriteOp>`.
1546
1923
  */
1547
- export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<TransactWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<TransactWriteOp> | Effect.Effect<ReadonlyArray<TransactWriteOp>, E2, R2>);
1924
+ export type AdditionalItemsInput<State, Event, E2 = never, R2 = never> = ReadonlyArray<AppendWriteOp> | ((decision: Decision<State, Event>) => ReadonlyArray<AppendWriteOp> | Effect.Effect<ReadonlyArray<AppendWriteOp>, E2, R2>);
1548
1925
  /**
1549
1926
  * The form `eventMetadata` ({@link CommandMetadataOptions}) accepts: a
1550
1927
  * function of the {@link Decision} returning one metadata entry per event
@@ -1743,20 +2120,20 @@ export type IdempotentCommandOptions<TMetadata, State = unknown, Event = unknown
1743
2120
  type CommandOptionsArgs<TMetadata, TConfig extends CommandHandlerOptions | undefined, State, TEvent, E2, R2, E3, R3, E4, R4> = TConfig extends {
1744
2121
  readonly idempotency: object;
1745
2122
  } ? [options: IdempotentCommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4>] : [options?: CommandOptions<TMetadata, State, TEvent, E2, R2, E3, R3, E4, R4> | undefined];
1746
- type CommandHandlerErrors<E> = E | VersionConflict | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
2123
+ type CommandHandlerErrors<E> = E | VersionConflict | StreamSealed | DuplicateCommand | AdditionalItemConditionFailed | AppendTooLarge | DynamoClientError | ValidationError | TransactionCancelled | UniqueConstraintViolation | OptimisticLockError | ConcurrentModification;
1747
2124
  /**
1748
2125
  * A handler over an {@link EventStream}. Generic per call over the error (`E2`)
1749
2126
  * and requirements (`R2`) of a function-form `additionalItems` that returns an
1750
2127
  * `Effect`, those (`E3` / `R3`) of an `eventMetadata` that does, and those
1751
2128
  * (`E4` / `R4`) of an `eventTimestamps` that does; all are `never` otherwise.
1752
2129
  */
1753
- 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>;
2130
+ 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 | LoadStateError<TConfig>, DynamoClient | TableConfig | R2 | R3 | R4 | LoadStateContext<TConfig>>;
1754
2131
  /**
1755
2132
  * A handler over a {@link BoundEventStream}: the stream's services are already
1756
2133
  * provided, so the `R2` / `R3` / `R4` of a function-form `additionalItems` /
1757
2134
  * `eventMetadata` / `eventTimestamps` are its only requirements.
1758
2135
  */
1759
- 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>;
2136
+ 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 | LoadStateError<TConfig>, R2 | R3 | R4 | LoadStateContext<TConfig>>;
1760
2137
  /**
1761
2138
  * The handler the data-last {@link commandHandler} returns for stream `S`: a
1762
2139
  * {@link BoundCommandHandler} for a `BoundEventStream` (checked first — a bound
@@ -1863,13 +2240,13 @@ type CommandHandlerFor<S, State, Command, TEvent, E, TConfig extends CommandHand
1863
2240
  * `EventStreamTypeId` brand of the second argument.
1864
2241
  */
1865
2242
  export declare const commandHandler: {
1866
- <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, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any, any, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
2243
+ <State, Command, TEvent, E, const TConfig extends CommandHandlerOptions<State> | undefined>(decider: Decider<State, Command, TEvent, E>, options?: TConfig): <S extends BoundEventStream<TEvent, any, any, State, any, any, any> | EventStream<TEvent, ReadonlyArray<string>, any, State, any, any, any>>(stream: S) => CommandHandlerFor<S, State, Command, TEvent, E, TConfig>;
1867
2244
  <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1868
2245
  readonly [K in TIndexName]: StreamIndexProjection;
1869
- }, TPartitionedIndexName extends string, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections, TPartitionedIndexName>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
2246
+ }, TPartitionedIndexName extends string, const TConfig extends CommandHandlerOptions<State, StreamIdInput<TStreamIdFields>> | undefined>(decider: Decider<State, Command, TEvent, E>, stream: BoundEventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections, TPartitionedIndexName>, options?: TConfig): BoundCommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
1870
2247
  <State, Command, TEvent, E, TStreamIdFields extends ReadonlyArray<string>, TMetadata, TState extends State, TIndexName extends string, TIndexProjections extends {
1871
2248
  readonly [K in TIndexName]: StreamIndexProjection;
1872
- }, TPartitionedIndexName extends string, const TConfig extends CommandHandlerOptions | undefined = undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections, TPartitionedIndexName>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
2249
+ }, TPartitionedIndexName extends string, const TConfig extends CommandHandlerOptions<State, StreamIdInput<TStreamIdFields>> | undefined>(decider: Decider<State, Command, TEvent, E>, stream: EventStream<TEvent, TStreamIdFields, TMetadata, TState, TIndexName, TIndexProjections, TPartitionedIndexName>, options?: TConfig): CommandHandler<State, Command, TEvent, E, TStreamIdFields, TMetadata, TConfig>;
1873
2250
  };
1874
2251
  /**
1875
2252
  * Reconstruct state from events by folding through a decider's `evolve` function.