effect-dynamodb 1.27.0 → 1.29.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 (61) hide show
  1. package/dist/Batch.d.ts.map +1 -1
  2. package/dist/Batch.js +3 -2
  3. package/dist/Batch.js.map +1 -1
  4. package/dist/Entity.d.ts +24 -1
  5. package/dist/Entity.d.ts.map +1 -1
  6. package/dist/Entity.js +33 -17
  7. package/dist/Entity.js.map +1 -1
  8. package/dist/EventStore.d.ts +66 -5
  9. package/dist/EventStore.d.ts.map +1 -1
  10. package/dist/EventStore.js +100 -2
  11. package/dist/EventStore.js.map +1 -1
  12. package/dist/Transaction.d.ts.map +1 -1
  13. package/dist/Transaction.js +5 -2
  14. package/dist/Transaction.js.map +1 -1
  15. package/dist/change-feed/ChangeFeed.d.ts +203 -0
  16. package/dist/change-feed/ChangeFeed.d.ts.map +1 -0
  17. package/dist/change-feed/ChangeFeed.js +498 -0
  18. package/dist/change-feed/ChangeFeed.js.map +1 -0
  19. package/dist/change-feed/CheckpointStore.d.ts +113 -0
  20. package/dist/change-feed/CheckpointStore.d.ts.map +1 -0
  21. package/dist/change-feed/CheckpointStore.js +247 -0
  22. package/dist/change-feed/CheckpointStore.js.map +1 -0
  23. package/dist/change-feed/DynamoStreamsClient.d.ts +71 -0
  24. package/dist/change-feed/DynamoStreamsClient.d.ts.map +1 -0
  25. package/dist/change-feed/DynamoStreamsClient.js +92 -0
  26. package/dist/change-feed/DynamoStreamsClient.js.map +1 -0
  27. package/dist/change-feed/Errors.d.ts +84 -0
  28. package/dist/change-feed/Errors.d.ts.map +1 -0
  29. package/dist/change-feed/Errors.js +40 -0
  30. package/dist/change-feed/Errors.js.map +1 -0
  31. package/dist/change-feed/index.d.ts +36 -0
  32. package/dist/change-feed/index.d.ts.map +1 -0
  33. package/dist/change-feed/index.js +36 -0
  34. package/dist/change-feed/index.js.map +1 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/internal/EntityOps.d.ts +7 -2
  40. package/dist/internal/EntityOps.d.ts.map +1 -1
  41. package/dist/internal/EntityOps.js.map +1 -1
  42. package/dist/internal/EventProjection.d.ts +782 -0
  43. package/dist/internal/EventProjection.d.ts.map +1 -0
  44. package/dist/internal/EventProjection.js +608 -0
  45. package/dist/internal/EventProjection.js.map +1 -0
  46. package/dist/internal/MultiProjection.d.ts +255 -0
  47. package/dist/internal/MultiProjection.d.ts.map +1 -0
  48. package/dist/internal/MultiProjection.js +377 -0
  49. package/dist/internal/MultiProjection.js.map +1 -0
  50. package/dist/internal/ProjectionEngine.d.ts +253 -0
  51. package/dist/internal/ProjectionEngine.d.ts.map +1 -0
  52. package/dist/internal/ProjectionEngine.js +837 -0
  53. package/dist/internal/ProjectionEngine.js.map +1 -0
  54. package/dist/internal/TransactWriteOps.d.ts.map +1 -1
  55. package/dist/internal/TransactWriteOps.js +12 -3
  56. package/dist/internal/TransactWriteOps.js.map +1 -1
  57. package/dist/internal/TransactableOps.d.ts +44 -1
  58. package/dist/internal/TransactableOps.d.ts.map +1 -1
  59. package/dist/internal/TransactableOps.js +52 -2
  60. package/dist/internal/TransactableOps.js.map +1 -1
  61. package/package.json +14 -3
@@ -0,0 +1,782 @@
1
+ /**
2
+ * EventStore projections (#151) — read models folded from a change feed that
3
+ * take their order from the event store, not from delivery.
4
+ *
5
+ * DynamoDB Streams orders records per item only, and a transaction's records
6
+ * carry no grouping or order, so a stream's events reach a consumer in any
7
+ * order. A projection groups a batch's events by row, sorts and
8
+ * deduplicates them, reads from the store only the versions the batch lacks
9
+ * (one strongly consistent `readFrom`), folds, and writes the read-model row
10
+ * once, conditioned on the checkpoint it read. See
11
+ * `docs/designs/eventstore-projections.md`.
12
+ *
13
+ * Re-exported from `EventStore`; this module only type-imports it.
14
+ *
15
+ * @internal
16
+ */
17
+ import { type ConcurrentModification, type ConditionalCheckFailed, type ItemNotFound, StreamRecordIdentifierMissing, type ValidationError } from "@effect-dynamodb/schema/Errors.js";
18
+ import { Duration, Effect, Option, Pipeable, type Schedule, type Schema } from "effect";
19
+ import type { DynamoClient, DynamoClientError } from "../DynamoClient.js";
20
+ import type { EventStream, StreamEvent, StreamIdInput, StreamMetadata } from "../EventStore.js";
21
+ import type { Table, TableConfig } from "../Table.js";
22
+ import type { EntityGet, EntityPut } from "./EntityOps.js";
23
+ import { type StreamImage } from "./Packed.js";
24
+ import { type DrainResult, type OutboxMeta, type RemoveRow } from "./ProjectionEngine.js";
25
+ /**
26
+ * The read model a projection writes: an unbound `Entity`. Its row is read
27
+ * with a strongly consistent `get` and written whole with `put` (or `create`
28
+ * for a new row), so the projection owns the row.
29
+ */
30
+ export interface ProjectionReadModel {
31
+ readonly entityType: string;
32
+ readonly model: Schema.Top;
33
+ readonly versioned: unknown;
34
+ readonly softDelete: unknown;
35
+ readonly timeSeries: unknown;
36
+ readonly get: (key: any) => EntityGet<any, any, any, any>;
37
+ readonly put: (input: any) => EntityPut<any, any, any, any>;
38
+ readonly create: (input: any) => EntityPut<any, any, any, any>;
39
+ readonly systemFields: {
40
+ readonly createdAt: string | null;
41
+ };
42
+ readonly _attachPrototype?: ((decoded: any) => any) | undefined;
43
+ }
44
+ /** The state a projection folds: the read model's model type. */
45
+ export type ProjectionState<RM> = RM extends {
46
+ readonly get: (key: any) => EntityGet<infer A, any, any, any>;
47
+ } ? A : never;
48
+ /** The read model's primary key, as its `get` takes it. */
49
+ export type ProjectionKey<RM> = RM extends {
50
+ readonly get: (key: infer K) => any;
51
+ } ? K : never;
52
+ /**
53
+ * What reading and writing the read-model row can fail with — its `get` (a
54
+ * missing row is not an error) and its `put`. The conditional write's
55
+ * `ConditionalCheckFailed` is the projection's own race signal, never surfaced.
56
+ */
57
+ export type ProjectionReadModelError<RM> = (RM extends {
58
+ readonly put: (input: any) => EntityPut<any, any, infer E, any>;
59
+ } ? Exclude<E, ConditionalCheckFailed> : never) | (RM extends {
60
+ readonly get: (key: any) => EntityGet<any, any, infer E, any>;
61
+ } ? Exclude<E, ItemNotFound> : never);
62
+ /** The read model's numeric fields — those a checkpoint may name. */
63
+ export type CheckpointField<S> = {
64
+ [K in keyof S]-?: S[K] extends number ? K : never;
65
+ }[keyof S] & string;
66
+ /**
67
+ * The read model's field holding the last applied stream version. With
68
+ * `{ field, offset }` the field stores `version + offset`: a read model that
69
+ * numbered versions from 0 adopts a projection with `offset: -1`, its stored
70
+ * values and readers unchanged.
71
+ */
72
+ export type ProjectionCheckpoint<S> = CheckpointField<S> | {
73
+ readonly field: CheckpointField<S>;
74
+ readonly offset?: number | undefined;
75
+ };
76
+ /** Configuration of {@link projection}. */
77
+ export interface ProjectionConfig<TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, RM extends ProjectionReadModel, E, R, KR extends ProjectionKey<RM> | Option.Option<ProjectionKey<RM>> = ProjectionKey<RM>, O = never> {
78
+ /** The source stream. One projection reads one stream. */
79
+ readonly stream: EventStream<TEvent, TStreamIdFields, TMetadata, any, any, any, any>;
80
+ /** The read model (an unbound `Entity`). */
81
+ readonly readModel: RM;
82
+ /**
83
+ * The row an event folds into. A function of the stream id alone gives one
84
+ * row per stream instance. One that also reads the event lets a stream feed
85
+ * several rows — each row is still fed by that one stream instance (a row
86
+ * fed by several streams needs a checkpoint per source, which projections
87
+ * don't keep). When a later event addresses a different row, the earlier
88
+ * row keeps what it had, and the new row folds only its own events.
89
+ *
90
+ * Return an `Option` to let an event address no row: `Option.none()`
91
+ * acknowledges it — no row is read or written, no checkpoint moves, and it
92
+ * is in no outcome (`ignored` lists its position). `apply` then returns an
93
+ * `Option` too.
94
+ */
95
+ readonly key: (streamId: StreamIdInput<TStreamIdFields>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => KR;
96
+ /**
97
+ * The state a row starts from, before `event` — the first event folded into
98
+ * a row that does not exist yet.
99
+ */
100
+ readonly initial: (streamId: StreamIdInput<TStreamIdFields>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => ProjectionState<RM>;
101
+ /**
102
+ * Apply one event. Return the state unchanged to ignore an event (the
103
+ * checkpoint still advances), or `EventStore.removeRow` to remove the row
104
+ * (with `removal`). A failure fails this row only, unless `terminal`
105
+ * accepts it. The library sets the checkpoint (and revision) after the
106
+ * fold; `evolve` need not. It runs again on every lost-race retry, so keep
107
+ * it free of side effects.
108
+ */
109
+ readonly evolve: (state: ProjectionState<RM>, event: StreamEvent<TEvent, StreamMetadata<TMetadata>>) => Effect.Effect<ProjectionState<RM> | RemoveRow, E, R>;
110
+ /** See {@link ProjectionCheckpoint}. */
111
+ readonly checkpoint: ProjectionCheckpoint<ProjectionState<RM>>;
112
+ /**
113
+ * The row as written, from the folded state (checkpoint set) — to recompute
114
+ * attributes kept for other readers (`Hidden` index fields, say) on every
115
+ * write. Default: the state as folded.
116
+ */
117
+ readonly toItem?: ((state: ProjectionState<RM>, context: {
118
+ readonly streamId: StreamIdInput<TStreamIdFields>;
119
+ readonly key: ProjectionKey<RM>;
120
+ }) => ProjectionState<RM>) | undefined;
121
+ /**
122
+ * The `evolve` failures that reject their event rather than fail the row —
123
+ * an update to a row that was never created, say. A rejected event is
124
+ * skipped and reported in the outcome's `rejected`; the fold continues, and
125
+ * a row that does not exist is not created by rejected events alone. The
126
+ * decision must be deterministic: a rejected event can be read again (to
127
+ * fill a later gap) and is rejected again. Any other failure fails the row,
128
+ * and keeps failing it on every redelivery.
129
+ *
130
+ * A row that is never created (every one of its events rejected) has no
131
+ * checkpoint, so each later delivery of its events gap-reads from version 1
132
+ * and reports the earlier rejections again — dedupe `rejected` by version
133
+ * when counting or logging them; the read grows with the stream.
134
+ */
135
+ readonly terminal?: ((error: E) => boolean) | undefined;
136
+ /**
137
+ * Retries after a lost race — another invocation advanced the row between
138
+ * this one's read and its conditional write. Each retry re-reads the row, so
139
+ * it usually finds the events applied and has nothing left to write. A
140
+ * number `n` is `Schedule.recurs(n)`. Default: up to 12 retries after a
141
+ * jittered delay growing from 5 ms by 1.5× (about 1.3 s in all). Exhausted,
142
+ * the row fails with `ConcurrentModification`.
143
+ */
144
+ readonly retry?: number | Schedule.Schedule<unknown, ConcurrentModification> | undefined;
145
+ /**
146
+ * A numeric model field the library owns: every write sets it one higher
147
+ * and is conditioned on the value it read (`attribute_not_exists` when the
148
+ * row has none). It may be an existing write counter readers already use.
149
+ * With it, a write is guarded by the revision instead of the checkpoint, so
150
+ * a `rebuild` can't be undone by a concurrent writer. Required by `outbox`.
151
+ * `{ field, initial }` sets the value a row without one is written with
152
+ * (default `1`) — `0`, say, to match a counter readers already see.
153
+ */
154
+ readonly revision?: ProjectionRevision<ProjectionState<RM>> | undefined;
155
+ /**
156
+ * Allow `evolve` to remove the row (`EventStore.removeRow`): the library
157
+ * writes a tombstone at the row's key that keeps its checkpoint and
158
+ * revision — read as no row, by this projection and by the entity — so a
159
+ * redelivery can't recreate it. `ttl` lets DynamoDB expire tombstones; it
160
+ * must exceed the change feed's 24 hours plus any redelivery or replay
161
+ * window, or a very late event recreates the row.
162
+ */
163
+ readonly removal?: {
164
+ readonly ttl?: Duration.Input | undefined;
165
+ } | undefined;
166
+ /** Side effects committed with the row — see {@link ProjectionOutboxConfig}. */
167
+ readonly outbox?: ProjectionOutboxConfig<O, StreamEvent<TEvent, StreamMetadata<TMetadata>>, ProjectionState<RM>> | undefined;
168
+ }
169
+ /** A projection's `revision` — the field, or the field and its starting value. */
170
+ export type ProjectionRevision<S> = CheckpointField<S> | {
171
+ readonly field: CheckpointField<S>;
172
+ readonly initial?: number | undefined;
173
+ };
174
+ /** @internal `revision`, resolved. */
175
+ export declare const resolveRevision: (revision: ProjectionRevision<any> | undefined) => {
176
+ readonly field: string;
177
+ readonly initial: number;
178
+ } | undefined;
179
+ /**
180
+ * An ordered, at-least-once outbox (#155). `effects(event, next, context)`
181
+ * returns the side effects an applied event produces (`next` is the row's
182
+ * state after it, `undefined` when it removed the row); they are written —
183
+ * encoded through `schema` — as outbox items in the same transaction as the
184
+ * row, so state and effects commit together. `drain` (or `deliver` on
185
+ * `applyBatch` / `processBatch`) hands them to a `deliver` function strictly
186
+ * in order per row, removing each only after it succeeds.
187
+ *
188
+ * Outbox items live in the row's own partition (sort key
189
+ * `<row sk>#outbox#<revision>#<n>`, `__edd_e__` `<entityType>#outbox`) — or
190
+ * in `table` when given (keyed by `keys`, default `pk` / `sk`). In the read
191
+ * model's table they show in its change stream and in full-table scans (not
192
+ * in the entity's reads, which filter on `__edd_e__`); a separate table keeps
193
+ * them out of both. `index` (a sparse index whose key attributes only outbox
194
+ * items carry) enables `drainPending`. A claim lasts `claimFor` (default 30
195
+ * seconds): a drainer that crashes after delivering leaves it to expire, and
196
+ * the effect is delivered again.
197
+ */
198
+ export interface ProjectionOutboxConfig<O, Ev, S> {
199
+ readonly schema: Schema.Codec<O, any>;
200
+ readonly effects: (event: Ev, next: S | undefined, context: {
201
+ readonly source: string;
202
+ readonly streamId: string;
203
+ }) => ReadonlyArray<O>;
204
+ readonly table?: Table<any, any> | undefined;
205
+ readonly keys?: {
206
+ readonly pk: string;
207
+ readonly sk: string;
208
+ } | undefined;
209
+ readonly index?: {
210
+ readonly name: string;
211
+ readonly pk: string;
212
+ readonly sk: string;
213
+ } | undefined;
214
+ readonly claimFor?: Duration.Input | undefined;
215
+ }
216
+ /** An event `evolve` rejected — see {@link ProjectionConfig.terminal}. */
217
+ export interface ProjectionRejected<A, M, E> {
218
+ readonly event: StreamEvent<A, M>;
219
+ readonly error: E;
220
+ }
221
+ /** One row applied by {@link EventProjection.applyBatch} / `apply`. */
222
+ export interface ProjectionApplied<S, A, M, K = unknown, E = unknown> {
223
+ /** The stream instance (`StreamEvent.streamId`). */
224
+ readonly streamId: string;
225
+ /** The row's key. */
226
+ readonly key: K;
227
+ /**
228
+ * The events this call applied to the row, in version order — including
229
+ * any it read from the store to fill a gap. Each event is in exactly one
230
+ * call's `applied`: the one whose conditional write won. Side effects driven
231
+ * from it are at most once (a crash after the write loses them).
232
+ */
233
+ readonly applied: ReadonlyArray<StreamEvent<A, M>>;
234
+ /**
235
+ * The events `evolve` rejected (see {@link ProjectionConfig.terminal}), in
236
+ * version order. Reported by every call that reads them — again, if a row
237
+ * that does not exist is not created before the next read.
238
+ */
239
+ readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
240
+ /**
241
+ * The batch's events this row had already applied (version at or below the
242
+ * checkpoint it read), in version order — a redelivery, or a late
243
+ * out-of-order delivery. Republishing their side effects gives at-least-once
244
+ * delivery, for consumers that tolerate duplicates.
245
+ */
246
+ readonly alreadyApplied: ReadonlyArray<StreamEvent<A, M>>;
247
+ /**
248
+ * The row as written, or as read when nothing was left to apply.
249
+ * `Option.none()` when there is no row: every event was rejected, or the
250
+ * row is removed (`removed`).
251
+ */
252
+ readonly state: Option.Option<S>;
253
+ /**
254
+ * The row's checkpoint, in store numbering (offset removed): every stream
255
+ * version up to it has been considered for this row.
256
+ */
257
+ readonly version: number;
258
+ /** The row is removed: a tombstone keeps its checkpoint (see `removal`). */
259
+ readonly removed: boolean;
260
+ /** Outbox effects committed with this write. */
261
+ readonly enqueued: number;
262
+ /**
263
+ * With `deliver`: the effect delivery stopped at — it stays pending, at the
264
+ * head of the row's outbox, for the next drain. The write itself succeeded.
265
+ */
266
+ readonly undelivered: Option.Option<{
267
+ readonly meta: OutboxMeta;
268
+ readonly error: unknown;
269
+ }>;
270
+ }
271
+ /** The outcome of one row in {@link ProjectionBatchResult}. */
272
+ export type ProjectionOutcome<S, A, M, Err, K = unknown, E = unknown> = ({
273
+ readonly _tag: "Applied";
274
+ /** Positions of this row's events in the `applyBatch` input, ascending. */
275
+ readonly positions: ReadonlyArray<number>;
276
+ } & ProjectionApplied<S, A, M, K, E>) | {
277
+ readonly _tag: "Failed";
278
+ readonly streamId: string;
279
+ /** The row's key — `undefined` when the stream id could not be parsed. */
280
+ readonly key: K | undefined;
281
+ /** Positions of this row's events in the `applyBatch` input, ascending. */
282
+ readonly positions: ReadonlyArray<number>;
283
+ readonly error: Err;
284
+ };
285
+ /** Result of {@link EventProjection.applyBatch}. */
286
+ export interface ProjectionBatchResult<S, A, M, Err, K = unknown, E = unknown> {
287
+ /** One outcome per row, in order of first delivery. */
288
+ readonly outcomes: ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>>;
289
+ /**
290
+ * The earliest input position of each failed row, ascending — the records
291
+ * to report in a partial batch response.
292
+ */
293
+ readonly failedPositions: ReadonlyArray<number>;
294
+ /** Input positions of the events whose `key` returned `Option.none()`, ascending. */
295
+ readonly ignored: ReadonlyArray<number>;
296
+ }
297
+ /** Result of {@link EventProjection.catchUp}. */
298
+ export interface ProjectionCaughtUp<S, A, M, K = unknown, E = unknown> {
299
+ /** The row of the stream's latest event — `Option.none()` for a stream with no events. */
300
+ readonly key: Option.Option<K>;
301
+ /** The events folded, in version order. Empty when the row was current. */
302
+ readonly applied: ReadonlyArray<StreamEvent<A, M>>;
303
+ /** The events `evolve` rejected, in version order. */
304
+ readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
305
+ /** The row — `Option.none()` when there is none, or it is removed. */
306
+ readonly state: Option.Option<S>;
307
+ readonly version: number;
308
+ /** The row is removed (a tombstone). */
309
+ readonly removed: boolean;
310
+ }
311
+ /** One row re-derived by {@link EventProjection.rebuild}. */
312
+ export interface ProjectionRebuiltRow<S, A, M, K = unknown, E = unknown> {
313
+ readonly key: K;
314
+ /** The row as written — as read, when every one of its events was rejected. */
315
+ readonly state: Option.Option<S>;
316
+ /** The version of the row's last event. */
317
+ readonly version: number;
318
+ readonly rejected: ReadonlyArray<ProjectionRejected<A, M, E>>;
319
+ /** The fold ended removed: a tombstone was written. */
320
+ readonly removed: boolean;
321
+ }
322
+ /** Result of {@link EventProjection.rebuild}. */
323
+ export interface ProjectionRebuilt<S, A = unknown, M = unknown, K = unknown, E = unknown> {
324
+ /** Every row the stream's events address, in order of their first event. Empty for a stream with no events. */
325
+ readonly rows: ReadonlyArray<ProjectionRebuiltRow<S, A, M, K, E>>;
326
+ }
327
+ /** What a projection's operations fail with: `evolve`'s `E`, the read model's errors, and the store's. */
328
+ export type ProjectionError<RM, E> = E | ProjectionReadModelError<RM> | DynamoClientError | ValidationError | ConcurrentModification;
329
+ /** Options of {@link EventProjection.applyBatch}. */
330
+ export interface ApplyBatchOptions {
331
+ /** Rows applied concurrently. Default `10`. */
332
+ readonly concurrency?: number | "unbounded" | undefined;
333
+ /**
334
+ * Drain each row that committed outbox effects right after its write,
335
+ * through this function (see {@link EventProjection.drain}). A delivery
336
+ * failure is reported in the outcome's `undelivered`; it never fails the
337
+ * row — the effects are committed, and stay pending for the next drain.
338
+ */
339
+ readonly deliver?: ((effect: any, meta: OutboxMeta) => Effect.Effect<void, any, any>) | undefined;
340
+ }
341
+ /**
342
+ * A DynamoDB Streams record as a Lambda event source mapping or an
343
+ * EventBridge Pipe delivers it — only the members the projection reads.
344
+ * Structural, so no `aws-lambda` types are needed. A record of another shape
345
+ * (a Pipe input template's) is read through `image` / `identifier` accessors.
346
+ */
347
+ export interface StreamRecord {
348
+ readonly eventID?: string | undefined;
349
+ readonly eventName?: string | undefined;
350
+ readonly dynamodb?: {
351
+ readonly SequenceNumber?: string | undefined;
352
+ readonly NewImage?: StreamImage | undefined;
353
+ } | undefined;
354
+ }
355
+ /**
356
+ * Who reads the partial batch response. A Lambda event source mapping on a
357
+ * DynamoDB stream identifies a record by `dynamodb.SequenceNumber`; an
358
+ * EventBridge Pipe by `eventID`.
359
+ */
360
+ export type BatchResponseSource = "lambda" | "pipe";
361
+ /** The `ReportBatchItemFailures` response. */
362
+ export interface BatchItemFailuresResponse {
363
+ readonly batchItemFailures: Array<{
364
+ readonly itemIdentifier: string;
365
+ }>;
366
+ }
367
+ /** Identify records by their source's member — see {@link BatchResponseSource}. */
368
+ export interface IdentifyBySource {
369
+ readonly source: BatchResponseSource;
370
+ }
371
+ /** Identify records with an accessor — for a record shape of your own. */
372
+ export interface IdentifyWith<Rec> {
373
+ readonly identifier: (record: Rec) => string | undefined;
374
+ }
375
+ /** Options of {@link EventProjection.processBatch}, beside how records are identified. */
376
+ export interface ProcessBatchOptions<Rec> extends ApplyBatchOptions {
377
+ /**
378
+ * A record's image, `undefined` to skip it. Default: the `NewImage` of an
379
+ * `INSERT` (a record without an `eventName` counts as one).
380
+ */
381
+ readonly image?: ((record: Rec) => StreamImage | undefined) | undefined;
382
+ /**
383
+ * What to do with a record whose image does not decode. A decode failure is
384
+ * deterministic, so `"report"` (the default) makes the source redeliver it
385
+ * — and everything after it on its shard — until its retries or age run out.
386
+ * `"skip"` acknowledges it, to log and dead-letter it yourself. Either way it
387
+ * is in `decodeFailures`.
388
+ *
389
+ * Skipping acknowledges the record, not the stored event: the next event of
390
+ * the same row gap-fills through it, fails to decode it, and fails the row
391
+ * (that record is reported). It helps when the bad event is its stream's
392
+ * last, or once the stored item is repaired or removed.
393
+ */
394
+ readonly onDecodeFailure?: "report" | "skip" | undefined;
395
+ }
396
+ /** Result of {@link EventProjection.processBatch}. */
397
+ export interface ProcessBatchResult<S, A, M, Err, Rec, K = unknown, E = unknown> {
398
+ /**
399
+ * See {@link ProjectionBatchResult.outcomes} — with `positions` indexing the
400
+ * **records** passed in.
401
+ */
402
+ readonly outcomes: ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>>;
403
+ /** Records whose image failed to decode. */
404
+ readonly decodeFailures: ReadonlyArray<{
405
+ readonly record: Rec;
406
+ readonly error: ValidationError;
407
+ }>;
408
+ /**
409
+ * The records the response reports, in delivery order: the earliest record
410
+ * of each failed row, and (with `onDecodeFailure: "report"`) every record
411
+ * that failed to decode.
412
+ */
413
+ readonly failedRecords: ReadonlyArray<Rec>;
414
+ /**
415
+ * The partial batch response for `failedRecords`. To fail more records (a
416
+ * side effect that failed, say), pass them with these to {@link batchResponse}.
417
+ */
418
+ readonly response: BatchItemFailuresResponse;
419
+ }
420
+ /**
421
+ * The partial batch response (`ReportBatchItemFailures`) for `failedRecords`:
422
+ * each identified by `dynamodb.SequenceNumber` for a Lambda event source
423
+ * mapping, by `eventID` for an EventBridge Pipe, or by your own `identifier`
424
+ * accessor.
425
+ *
426
+ * Lambda checkpoints at the lowest reported sequence number and redelivers
427
+ * every record from there, so reporting the earliest failed record of each
428
+ * row is enough. A record without an identifier fails with
429
+ * `StreamRecordIdentifierMissing` — an empty one would fail the whole batch
430
+ * silently — so fail the invocation, and the source redelivers the batch.
431
+ */
432
+ export declare const batchResponse: {
433
+ (failedRecords: ReadonlyArray<StreamRecord>, options: IdentifyBySource): Effect.Effect<BatchItemFailuresResponse, StreamRecordIdentifierMissing>;
434
+ <Rec>(failedRecords: ReadonlyArray<Rec>, options: IdentifyWith<Rec>): Effect.Effect<BatchItemFailuresResponse, StreamRecordIdentifierMissing>;
435
+ };
436
+ /** A stream {@link decoder} can dispatch to — an `EventStream` or a `BoundEventStream`. */
437
+ export interface DecodableStream {
438
+ readonly streamName: string;
439
+ readonly eventMarker: string;
440
+ decodeStreamImage(image: StreamImage): Effect.Effect<Option.Option<unknown>, ValidationError, any>;
441
+ }
442
+ /** The event a decodable stream decodes to. */
443
+ type DecodedEventOf<S> = S extends {
444
+ decodeStreamImage(image: any, options?: any): Effect.Effect<Option.Option<infer Ev>, any, any>;
445
+ } ? Ev : never;
446
+ /** What {@link decoder} returns for one of its streams' event images. */
447
+ export type DecodedStreamEvent<Streams extends {
448
+ readonly [name: string]: DecodableStream;
449
+ }> = {
450
+ readonly [K in keyof Streams & string]: {
451
+ readonly stream: K;
452
+ readonly event: DecodedEventOf<Streams[K]>;
453
+ };
454
+ }[keyof Streams & string];
455
+ /**
456
+ * One decoder for several streams' change-feed images. Name the streams in a
457
+ * record; `decode(image)` dispatches on the image's `__edd_e__` and returns
458
+ * `{ stream: <name>, event }`, or `Option.none()` for anything that is not one
459
+ * of their events (snapshots, idempotency sentinels, entities, other streams).
460
+ * The image is decoded by that stream's `decodeStreamImage`, so it accepts the
461
+ * same shapes (DynamoDB JSON with `B` as base64 or bytes, or unmarshalled).
462
+ *
463
+ * Two streams storing events under the same marker throw `[EDD-9082]`.
464
+ *
465
+ * @example
466
+ * ```ts
467
+ * const decode = EventStore.decoder({ Order: OrderEvents, Payment: PaymentEvents })
468
+ * const decoded = yield* decode(record.dynamodb.NewImage)
469
+ * if (Option.isSome(decoded) && decoded.value.stream === "Order") { ... }
470
+ * ```
471
+ */
472
+ export declare const decoder: <const Streams extends {
473
+ readonly [name: string]: DecodableStream;
474
+ }>(streams: Streams) => ((image: StreamImage) => Effect.Effect<Option.Option<DecodedStreamEvent<Streams>>, ValidationError, never>);
475
+ /** A Lambda event source mapping / EventBridge Pipe filter pattern. */
476
+ export interface EventFilterPattern {
477
+ readonly eventName: ReadonlyArray<"INSERT">;
478
+ readonly dynamodb: {
479
+ readonly NewImage: {
480
+ readonly __edd_e__: {
481
+ readonly S: ReadonlyArray<string>;
482
+ };
483
+ };
484
+ };
485
+ }
486
+ /**
487
+ * The filter pattern that selects the `INSERT`s of these streams' event items
488
+ * — `JSON.stringify` it into `FilterCriteria.Filters[].Pattern` of a Lambda
489
+ * event source mapping or an EventBridge Pipe. Snapshots, idempotency
490
+ * sentinels and other items are filtered out before the function runs.
491
+ */
492
+ export declare const eventFilterPattern: (streams: ReadonlyArray<DecodableStream> | {
493
+ readonly [name: string]: DecodableStream;
494
+ }) => EventFilterPattern;
495
+ declare const EventProjectionTypeId: unique symbol;
496
+ export type EventProjectionTypeId = typeof EventProjectionTypeId;
497
+ /**
498
+ * @internal How a projection takes part in a multi-projection batch: one
499
+ * route per source stream — its decode (under the bound context for a bound
500
+ * projection) and its `applyBatch`.
501
+ */
502
+ export declare const ProjectionRoutes: unique symbol;
503
+ /**
504
+ * A change-feed projection: one source stream folded into read-model rows —
505
+ * see {@link projection}. Its operations need `DynamoClient | TableConfig`
506
+ * (the stream's table and the read model's) and `evolve`'s requirements;
507
+ * {@link bindProjection} resolves them (`R = never`).
508
+ */
509
+ export interface EventProjection<S, A, TStreamIdFields extends ReadonlyArray<string>, M, Err, R, K = unknown, E = unknown, I extends boolean = false, O = never> extends Pipeable.Pipeable {
510
+ readonly [EventProjectionTypeId]: EventProjectionTypeId;
511
+ /** The source stream. */
512
+ readonly stream: EventStream<A, TStreamIdFields, any, any, any, any, any>;
513
+ /** @internal See {@link ProjectionRoutes}. */
514
+ readonly [ProjectionRoutes]: (name: string) => ReadonlyArray<Route>;
515
+ /** The read model's entity type. */
516
+ readonly entityType: string;
517
+ /**
518
+ * Apply one stream's decoded change-feed events — a batch, in delivery
519
+ * order. Per row (`key(streamId, event)`, concurrently across rows):
520
+ *
521
+ * 1. sort and deduplicate the row's events by version;
522
+ * 2. read the row (strongly consistent) and its checkpoint `c`; the events
523
+ * at or below `c` are `alreadyApplied`, and with none left the row is
524
+ * done;
525
+ * 3. if the rest are not exactly `c + 1 …` their highest version, read
526
+ * `c + 1 …` that version from the store with one strongly consistent
527
+ * `readFrom` and keep the row's events — so nothing waits for a delivery;
528
+ * 4. fold through `evolve` (skipping `terminal` rejections), set the
529
+ * checkpoint, and write the row once, conditioned on the checkpoint still
530
+ * being `c` (`create` for a new row; nothing when a new row's events were
531
+ * all rejected);
532
+ * 5. on a lost race, re-read and continue, under the `retry` policy.
533
+ *
534
+ * Never fails: each row's failure is its outcome, so one row's error can't
535
+ * hide the others' `applied`.
536
+ */
537
+ applyBatch(events: ReadonlyArray<StreamEvent<A, M>>, options?: ApplyBatchOptions | undefined): Effect.Effect<ProjectionBatchResult<S, A, M, Err, K, E>, never, R>;
538
+ /** {@link EventProjection.applyBatch} of one event, its failure in the error channel. */
539
+ apply(event: StreamEvent<A, M>): Effect.Effect<I extends true ? Option.Option<ProjectionApplied<S, A, M, K, E>> : ProjectionApplied<S, A, M, K, E>, Err, R>;
540
+ /**
541
+ * Bring the row of the stream's latest event up to the head: read that
542
+ * event (one strongly consistent `Query`), then the row's events after its
543
+ * checkpoint (one strongly consistent `readFrom`) — to repair a row whose
544
+ * last event never arrived (a dead-lettered record, say). An earlier missing
545
+ * event is filled in by the next delivery anyway. With one row per stream
546
+ * instance that is the only row; {@link EventProjection.rebuild} re-derives
547
+ * every row.
548
+ */
549
+ catchUp(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<ProjectionCaughtUp<S, A, M, K, E>, Err, R>;
550
+ /**
551
+ * Re-derive every row of a stream instance from version 1: fold the whole
552
+ * stream (strongly consistent) from `initial`, row by row, ignoring the rows'
553
+ * state, and write each conditioned on the checkpoint read. Returns no
554
+ * applied events, so a rebuild never replays side effects. A stream with no
555
+ * events writes nothing.
556
+ *
557
+ * Pause live delivery for the stream instance while it runs. The condition
558
+ * is the checkpoint, and a rebuild of a row already at the stream's head
559
+ * writes the same checkpoint back — so an `applyBatch` that read the row
560
+ * before the rebuild still holds a matching condition, and its later write
561
+ * (folded from the old state) replaces the rebuilt row.
562
+ */
563
+ rebuild(streamId: StreamIdInput<TStreamIdFields>): Effect.Effect<ProjectionRebuilt<S, A, M, K, E>, Err, R>;
564
+ /**
565
+ * Process a raw change-feed batch: decode each `INSERT`'s `NewImage` (other
566
+ * records, and images that are not this stream's events, are skipped), apply
567
+ * the events with {@link EventProjection.applyBatch}, and build the partial
568
+ * batch response — the earliest record of each failed row and every record
569
+ * that failed to decode (unless `onDecodeFailure: "skip"`), identified per
570
+ * `options.source` or by `options.identifier`. Outcome `positions` index
571
+ * `records`. A failed record without an identifier fails the call with
572
+ * `StreamRecordIdentifierMissing`: fail the invocation, and the source
573
+ * redelivers the batch.
574
+ */
575
+ processBatch<Rec extends StreamRecord>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>)): Effect.Effect<ProcessBatchResult<S, A, M, Err, Rec, K, E>, StreamRecordIdentifierMissing, R>;
576
+ /** A record shape of your own, read through `image` and `identifier`. */
577
+ processBatch<Rec>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & {
578
+ readonly image: (record: Rec) => StreamImage | undefined;
579
+ } & IdentifyWith<Rec>): Effect.Effect<ProcessBatchResult<S, A, M, Err, Rec, K, E>, StreamRecordIdentifierMissing, R>;
580
+ /**
581
+ * Deliver a row's pending outbox effects (see `outbox`), strictly in order:
582
+ * claim the head item, `deliver` it, delete it, repeat — stopping at a
583
+ * failure (reported in `undelivered`, the effect left pending) or at a head
584
+ * another drainer holds. A crash after delivering leaves a claim that
585
+ * expires (`claimFor`), and the effect is delivered again: at least once.
586
+ */
587
+ drain<DE, DR>(key: K, deliver: (effect: O, meta: OutboxMeta) => Effect.Effect<void, DE, DR>): Effect.Effect<DrainResult<DE>, DynamoClientError | ValidationError, R | DR>;
588
+ /**
589
+ * Drain every row with pending effects, found through the outbox's sparse
590
+ * `index` — the sweeper for rows that receive no further events.
591
+ */
592
+ drainPending<DE, DR>(deliver: (effect: O, meta: OutboxMeta) => Effect.Effect<void, DE, DR>): Effect.Effect<{
593
+ readonly delivered: number;
594
+ readonly undelivered: ReadonlyArray<{
595
+ readonly meta: OutboxMeta;
596
+ readonly error: DE;
597
+ }>;
598
+ }, DynamoClientError | ValidationError, R | DR>;
599
+ }
600
+ /** @internal Refusals shared by `projection` and `multiProjection` (`[EDD-9081]`). */
601
+ export declare const refuseReadModel: (label: string, readModel: ProjectionReadModel, options: {
602
+ readonly fields: ReadonlyArray<string>;
603
+ readonly removal: boolean;
604
+ readonly outbox: boolean;
605
+ readonly revision: string | undefined;
606
+ }) => void;
607
+ /** @internal An outbox config, resolved. */
608
+ export declare const resolveOutbox: (outbox: ProjectionOutboxConfig<any, any, any> | undefined) => {
609
+ schema: Schema.Codec<any, any, never, never>;
610
+ effects: (event: any, next: any, context: {
611
+ readonly source: string;
612
+ readonly streamId: string;
613
+ }) => readonly any[];
614
+ table: Table<any, any> | undefined;
615
+ pk: string;
616
+ sk: string;
617
+ index: {
618
+ readonly name: string;
619
+ readonly pk: string;
620
+ readonly sk: string;
621
+ } | undefined;
622
+ claimFor: Duration.Duration;
623
+ } | undefined;
624
+ /**
625
+ * Define a change-feed projection of `stream` into `readModel` — rows keyed by
626
+ * `key(streamId, event)` (one per stream instance when the key ignores the
627
+ * event), each folded from `initial(streamId, event)` through `evolve`, its
628
+ * checkpoint kept in `checkpoint`. See {@link EventProjection} for the
629
+ * operations.
630
+ *
631
+ * The read model must be a plain entity: `versioned` (the checkpoint is the
632
+ * version), `softDelete` (removal is `removal` tombstones) and `timeSeries`
633
+ * read models throw `[EDD-9081]`, as do a field the model lacks, a
634
+ * non-integer offset, `removal` / `outbox` on a read model with `unique`
635
+ * constraints, and `outbox` without `revision`.
636
+ *
637
+ * @example
638
+ * ```ts
639
+ * const OrderSummaryProjection = EventStore.projection({
640
+ * stream: OrderEvents,
641
+ * readModel: OrderSummaries,
642
+ * key: (id) => ({ orderId: id.orderId }),
643
+ * initial: (id) => ({ orderId: id.orderId, status: "open", total: 0, version: 0 }),
644
+ * evolve: (state, event) => Effect.succeed(next(state, event.data)),
645
+ * checkpoint: "version",
646
+ * })
647
+ * ```
648
+ */
649
+ export declare const projection: <TEvent, TStreamIdFields extends ReadonlyArray<string>, TMetadata, RM extends ProjectionReadModel, E = never, R = never, KR extends ProjectionKey<RM> | Option.Option<ProjectionKey<RM>> = ProjectionKey<RM>, O = never>(config: ProjectionConfig<TEvent, TStreamIdFields, TMetadata, RM, E, R, KR, O>) => EventProjection<ProjectionState<RM>, TEvent, TStreamIdFields, StreamMetadata<TMetadata>, ProjectionError<RM, E>, R | DynamoClient | TableConfig, ProjectionKey<RM>, E, [Extract<KR, Option.Option<unknown>>] extends [never] ? false : true, O>;
650
+ /** Any projection, bound or unbound — single-source, or multi-source. */
651
+ export type AnyEventProjection = EventProjection<any, any, any, any, any, any, any, any, any, any> | {
652
+ readonly [ProjectionRoutes]: (name: string) => ReadonlyArray<Route>;
653
+ applyBatch(events: ReadonlyArray<any>, options?: any): Effect.Effect<any, never, any>;
654
+ };
655
+ /** A projection's outcomes. */
656
+ export type ProjectionOutcomesOf<P> = P extends EventProjection<infer S, infer A, any, infer M, infer Err, any, infer K, infer E, any, any> ? ReadonlyArray<ProjectionOutcome<S, A, M, Err, K, E>> : P extends {
657
+ applyBatch(events: any, options?: any): Effect.Effect<{
658
+ readonly outcomes: infer Outcomes;
659
+ }, never, any>;
660
+ } ? Outcomes : never;
661
+ /** A projection's requirements. */
662
+ export type ProjectionRequirementsOf<P> = P extends EventProjection<any, any, any, any, any, infer R, any, any, any, any> ? R : P extends {
663
+ applyBatch(events: any, options?: any): Effect.Effect<any, never, infer R>;
664
+ } ? R : never;
665
+ /** Result of {@link processBatch}. */
666
+ export interface ProcessBatchesResult<Projections extends {
667
+ readonly [name: string]: AnyEventProjection;
668
+ }, Rec, Streams extends {
669
+ readonly [name: string]: DecodableStream;
670
+ } = {}> {
671
+ /**
672
+ * The events of the `streams` named without a projection, in delivery order,
673
+ * each with its record's position — so a caller acting on them needs no
674
+ * second decode.
675
+ */
676
+ readonly unrouted: ReadonlyArray<{
677
+ readonly position: number;
678
+ } & DecodedStreamEvent<Streams>>;
679
+ /**
680
+ * Each projection's outcomes — `positions` index the **records** passed in.
681
+ * A projection whose stream had no events in the batch has none.
682
+ */
683
+ readonly outcomes: {
684
+ readonly [K in keyof Projections]: ProjectionOutcomesOf<Projections[K]>;
685
+ };
686
+ /** Records whose image failed to decode. */
687
+ readonly decodeFailures: ReadonlyArray<{
688
+ readonly record: Rec;
689
+ readonly error: ValidationError;
690
+ }>;
691
+ /**
692
+ * The records the response reports, in delivery order: the earliest record
693
+ * of each failed row of every projection, and (with
694
+ * `onDecodeFailure: "report"`) every record that failed to decode.
695
+ */
696
+ readonly failedRecords: ReadonlyArray<Rec>;
697
+ /** The partial batch response for `failedRecords`. */
698
+ readonly response: BatchItemFailuresResponse;
699
+ }
700
+ /** @internal One projection behind a feed: the stream it decodes and its `applyBatch`. */
701
+ export interface Route {
702
+ readonly name: string;
703
+ /**
704
+ * A multi-source projection's name for this stream: its events reach
705
+ * `applyBatch` as `{ stream: tag, event }`. Undefined: as the bare event.
706
+ */
707
+ readonly tag?: string | undefined;
708
+ /** The (unbound) stream — its identity groups the projections that read it. */
709
+ readonly stream: DecodableStream;
710
+ /** How its images decode — under the bound context for a bound projection. */
711
+ readonly decode: (image: StreamImage) => Effect.Effect<Option.Option<unknown>, ValidationError, any>;
712
+ readonly applyBatch: (events: ReadonlyArray<any>, options?: ApplyBatchOptions | undefined) => Effect.Effect<ProjectionBatchResult<any, any, any, any, any, any>, never, any>;
713
+ }
714
+ /**
715
+ * @internal Decode a batch once, route each event to the projections of its
716
+ * stream, apply each projection's events, and build one partial batch
717
+ * response — shared by {@link processBatch} and `projection.processBatch`.
718
+ */
719
+ export declare const runBatch: <Rec>(operation: string, records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>), routes: ReadonlyArray<Route>,
720
+ /** Streams decoded without a projection: their events are returned as `unrouted`. */
721
+ decodeOnly?: ReadonlyArray<readonly [string, DecodableStream]>) => Effect.Effect<{
722
+ readonly unrouted: ReadonlyArray<{
723
+ readonly position: number;
724
+ readonly stream: string;
725
+ readonly event: unknown;
726
+ }>;
727
+ readonly outcomes: Record<string, ReadonlyArray<ProjectionOutcome<any, any, any, any, any, any>>>;
728
+ readonly decodeFailures: ReadonlyArray<{
729
+ readonly record: Rec;
730
+ readonly error: ValidationError;
731
+ }>;
732
+ readonly failedRecords: ReadonlyArray<Rec>;
733
+ readonly response: BatchItemFailuresResponse;
734
+ }, StreamRecordIdentifierMissing, any>;
735
+ /**
736
+ * Process one change-feed batch for several projections — the usual shape
737
+ * when one table's feed carries several streams. Each record is decoded once
738
+ * and routed to every projection of its stream; records of other streams and
739
+ * items are skipped, as acknowledged. Each projection applies its events
740
+ * (see {@link EventProjection.applyBatch}), and one partial batch response
741
+ * names the earliest record of every failed row across all of them, plus the
742
+ * records that failed to decode (unless `onDecodeFailure: "skip"`). Outcome
743
+ * `positions` index `records`.
744
+ *
745
+ * Records are read as `StreamRecord`s, identified per `source`, or through
746
+ * `image` / `identifier` accessors for a shape of your own (a Pipe input
747
+ * template's). A failed record without an identifier fails the call with
748
+ * `StreamRecordIdentifierMissing`. `streams` names streams to decode without a
749
+ * projection: their events come back as `unrouted`. Two streams storing
750
+ * events under one marker die with `[EDD-9082]`.
751
+ *
752
+ * @example
753
+ * ```ts
754
+ * const result = yield* EventStore.processBatch(event.Records, {
755
+ * projections: { summaries: OrderSummaryProjection, balances: BalanceProjection },
756
+ * source: "lambda",
757
+ * })
758
+ * return result.response
759
+ * ```
760
+ */
761
+ export declare const processBatch: {
762
+ <const Projections extends {
763
+ readonly [name: string]: AnyEventProjection;
764
+ }, Rec extends StreamRecord, const Streams extends {
765
+ readonly [name: string]: DecodableStream;
766
+ } = {}>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & (IdentifyBySource | IdentifyWith<Rec>) & {
767
+ readonly projections: Projections;
768
+ readonly streams?: Streams | undefined;
769
+ }): Effect.Effect<ProcessBatchesResult<Projections, Rec, Streams>, StreamRecordIdentifierMissing, ProjectionRequirementsOf<Projections[keyof Projections]>>;
770
+ <const Projections extends {
771
+ readonly [name: string]: AnyEventProjection;
772
+ }, Rec, const Streams extends {
773
+ readonly [name: string]: DecodableStream;
774
+ } = {}>(records: ReadonlyArray<Rec>, options: ProcessBatchOptions<Rec> & {
775
+ readonly image: (record: Rec) => StreamImage | undefined;
776
+ } & IdentifyWith<Rec> & {
777
+ readonly projections: Projections;
778
+ readonly streams?: Streams | undefined;
779
+ }): Effect.Effect<ProcessBatchesResult<Projections, Rec, Streams>, StreamRecordIdentifierMissing, ProjectionRequirementsOf<Projections[keyof Projections]>>;
780
+ };
781
+ export {};
782
+ //# sourceMappingURL=EventProjection.d.ts.map